mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-17 18:51:43 -05:00
`activeTurns` was previously a horrible hack, used for "first full turn only" effects like Speed Boost as well as "first move action" effects like Fake Out. In addition to being a huge hazard for API users such as OMs, this also means weird bugs such as Speed Boost not working if you get hit by Sky Drop on your first turn. This commit fixes them by splitting these counters into two - an `activeTurns` counter for Speed Boost, and an `activeMoveActions` counter for Fake Out.
1376 lines
48 KiB
JavaScript
1376 lines
48 KiB
JavaScript
'use strict';
|
||
|
||
const CHOOSABLE_TARGETS = new Set(['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe']);
|
||
|
||
/**@type {BattleScriptsData} */
|
||
let BattleScripts = {
|
||
gen: 8,
|
||
/**
|
||
* 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(moveOrMoveName, pokemon, targetLoc, sourceEffect, zMove, externalMove, maxMove, originalTarget) {
|
||
pokemon.activeMoveActions++;
|
||
let target = this.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget);
|
||
let baseMove = this.dex.getActiveMove(moveOrMoveName);
|
||
const pranksterBoosted = baseMove.pranksterBoosted;
|
||
if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) {
|
||
let 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 = baseMove;
|
||
if (zMove) {
|
||
move = this.getActiveZMove(baseMove, pokemon);
|
||
} else if (maxMove) {
|
||
move = this.getActiveMaxMove(baseMove, pokemon);
|
||
}
|
||
|
||
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;
|
||
} */
|
||
let 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);
|
||
let 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
|
||
let 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.side.zMoveUsed = true;
|
||
}
|
||
let moveDidSomething = this.useMove(baseMove, pokemon, target, sourceEffect, zMove, maxMove);
|
||
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) {
|
||
let 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');
|
||
// @ts-ignore - the Dancer ability can't trigger on a move where target is null because it does not copy failed moves.
|
||
const dancersTarget = target.side !== dancer.side && pokemon.side === dancer.side ? target : pokemon;
|
||
// @ts-ignore
|
||
this.runMove(move.id, dancer, this.getTargetLoc(dancersTarget, dancer), this.dex.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(move, pokemon, target, sourceEffect, zMove, maxMove) {
|
||
pokemon.moveThisTurnResult = undefined;
|
||
/** @type {boolean? | undefined} */ // Typescript bug
|
||
let oldMoveResult = pokemon.moveThisTurnResult;
|
||
let moveResult = this.useMoveInner(move, pokemon, target, sourceEffect, zMove, maxMove);
|
||
if (oldMoveResult === pokemon.moveThisTurnResult) pokemon.moveThisTurnResult = moveResult;
|
||
return moveResult;
|
||
},
|
||
useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove, maxMove) {
|
||
if (!sourceEffect && this.effect.id) sourceEffect = this.effect;
|
||
if (sourceEffect && ['instruct', 'custapberry'].includes(sourceEffect.id)) sourceEffect = null;
|
||
|
||
let move = this.dex.getActiveMove(moveOrMoveName);
|
||
if (move.id === 'weatherball' && zMove) {
|
||
// Z-Weather Ball only changes types if it's used directly,
|
||
// not if it's called by Z-Sleep Talk or something.
|
||
this.singleEvent('ModifyType', move, null, pokemon, target, move, move);
|
||
if (move.type !== 'Normal') sourceEffect = move;
|
||
}
|
||
if (zMove || (move.category !== 'Status' && sourceEffect && /** @type {ActiveMove} */(sourceEffect).isZ)) {
|
||
move = this.getActiveZMove(move, pokemon);
|
||
}
|
||
if (maxMove && move.category !== 'Status') {
|
||
let moveType = move.type;
|
||
// Max move outcome is dependent on the move type after type modifications from ability and the move itself
|
||
this.singleEvent('ModifyType', move, null, pokemon, target, move, move);
|
||
this.runEvent('ModifyType', pokemon, target, move, move);
|
||
if (move.type !== moveType) sourceEffect = move;
|
||
}
|
||
if (maxMove || (move.category !== 'Status' && sourceEffect && /** @type {ActiveMove} */(sourceEffect).isMax)) {
|
||
move = this.getActiveMaxMove(move, pokemon);
|
||
}
|
||
|
||
if (this.activeMove) {
|
||
move.priority = this.activeMove.priority;
|
||
if (!move.hasBounced) move.pranksterBoosted = this.activeMove.pranksterBoosted;
|
||
}
|
||
let baseTarget = move.target;
|
||
if (target === undefined) target = this.getRandomTarget(pokemon, move);
|
||
if (move.target === 'self' || move.target === 'allies') {
|
||
target = pokemon;
|
||
}
|
||
if (sourceEffect) {
|
||
move.sourceEffect = sourceEffect.id;
|
||
move.ignoreAbility = false;
|
||
}
|
||
let moveResult = false;
|
||
|
||
this.setActiveMove(move, pokemon, target);
|
||
|
||
this.singleEvent('ModifyType', move, null, pokemon, target, move, move);
|
||
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.getRandomTarget(pokemon, move);
|
||
}
|
||
move = this.runEvent('ModifyType', pokemon, target, move, move);
|
||
move = this.runEvent('ModifyMove', pokemon, target, move, move);
|
||
if (baseTarget !== move.target) {
|
||
// Adjust again
|
||
target = this.getRandomTarget(pokemon, move);
|
||
}
|
||
if (!move || pokemon.fainted) {
|
||
return false;
|
||
}
|
||
|
||
let attrs = '';
|
||
|
||
let movename = move.name;
|
||
if (move.id === 'hiddenpower') movename = 'Hidden Power';
|
||
if (sourceEffect) attrs += '|[from]' + this.dex.getEffect(sourceEffect);
|
||
if (zMove && move.isZ === true) {
|
||
attrs = '|[anim]' + movename + attrs;
|
||
movename = 'Z-' + movename;
|
||
}
|
||
this.addMove('move', pokemon, movename, target + attrs);
|
||
|
||
if (zMove) this.runZPower(move, pokemon);
|
||
|
||
if (!target) {
|
||
this.attrLastMove('[notarget]');
|
||
this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon);
|
||
return false;
|
||
}
|
||
|
||
const {targets, pressureTargets} = pokemon.getMoveTargets(move, target);
|
||
|
||
if (!sourceEffect || sourceEffect.id === 'pursuit') {
|
||
let extraPP = 0;
|
||
for (const source of pressureTargets) {
|
||
let ppDrop = this.runEvent('DeductPP', source, pokemon, move);
|
||
if (ppDrop !== true) {
|
||
extraPP += ppDrop || 0;
|
||
}
|
||
}
|
||
if (extraPP > 0) {
|
||
pokemon.deductPP(move, extraPP);
|
||
}
|
||
}
|
||
|
||
if (!this.singleEvent('TryMove', move, null, pokemon, target, move) ||
|
||
!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 (this.gen !== 4 && move.selfdestruct === 'always') {
|
||
this.faint(pokemon, pokemon, move);
|
||
}
|
||
|
||
/** @type {number | false | undefined | ''} */
|
||
let damage = false;
|
||
if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') {
|
||
damage = this.tryMoveHit(target, pokemon, move);
|
||
if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null;
|
||
if (damage || damage === 0 || damage === undefined) moveResult = true;
|
||
} else {
|
||
if (!targets.length) {
|
||
this.attrLastMove('[notarget]');
|
||
this.add(this.gen >= 5 ? '-fail' : '-notarget', pokemon);
|
||
return false;
|
||
}
|
||
if (this.gen === 4 && move.selfdestruct === 'always') {
|
||
this.faint(pokemon, pokemon, move);
|
||
}
|
||
moveResult = this.trySpreadMoveHit(targets, pokemon, move);
|
||
}
|
||
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;
|
||
},
|
||
/** NOTE: includes single-target moves */
|
||
trySpreadMoveHit(targets, pokemon, move) {
|
||
if (targets.length > 1 && !move.smartTarget) move.spreadHit = true;
|
||
|
||
/** @type {((targets: Pokemon[], pokemon: Pokemon, move: ActiveMove) => (number | boolean | "" | undefined)[] | undefined)[]} */
|
||
let moveSteps = [
|
||
// 0. check for semi invulnerability
|
||
this.hitStepInvulnerabilityEvent,
|
||
|
||
// 1. run the 'TryHit' event (Protect, Magic Bounce, Volt Absorb, etc.) (this is step 2 in gens 5 & 6, and step 4 in gen 4)
|
||
this.hitStepTryHitEvent,
|
||
|
||
// 2. check for type immunity (this is step 1 in gens 4-6)
|
||
this.hitStepTypeImmunity,
|
||
|
||
// 3. check for various move-specific immunities
|
||
this.hitStepTryImmunity,
|
||
|
||
// 4. check accuracy
|
||
this.hitStepAccuracy,
|
||
|
||
// 5. break protection effects
|
||
this.hitStepBreakProtect,
|
||
|
||
// 6. steal positive boosts (Spectral Thief)
|
||
this.hitStepStealBoosts,
|
||
|
||
// 7. loop that processes each hit of the move (has its own steps per iteration)
|
||
this.hitStepMoveHitLoop,
|
||
];
|
||
if (this.gen <= 6) {
|
||
// Swap step 1 with step 2
|
||
[moveSteps[1], moveSteps[2]] = [moveSteps[2], moveSteps[1]];
|
||
}
|
||
if (this.gen === 4) {
|
||
// Swap step 4 with new step 2 (old step 1)
|
||
[moveSteps[2], moveSteps[4]] = [moveSteps[4], moveSteps[2]];
|
||
}
|
||
|
||
this.setActiveMove(move, pokemon, targets[0]);
|
||
|
||
let hitResult = this.singleEvent('PrepareHit', move, {}, targets[0], pokemon, move);
|
||
if (!hitResult) {
|
||
if (hitResult === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
}
|
||
this.runEvent('PrepareHit', pokemon, targets[0], move);
|
||
|
||
hitResult = this.singleEvent('Try', move, null, pokemon, targets[0], move);
|
||
if (!hitResult) {
|
||
if (hitResult === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
}
|
||
|
||
let atLeastOneFailure;
|
||
for (const step of moveSteps) {
|
||
/** @type {(number | boolean | "" | undefined)[] | undefined} */
|
||
let hitResults = step.call(this, targets, pokemon, move);
|
||
if (!hitResults) continue;
|
||
// @ts-ignore
|
||
targets = targets.filter((val, i) => hitResults[i] || hitResults[i] === 0);
|
||
atLeastOneFailure = atLeastOneFailure || hitResults.some(val => val === false);
|
||
if (!targets.length) {
|
||
// console.log(step.name);
|
||
break;
|
||
}
|
||
}
|
||
|
||
const moveResult = !!targets.length;
|
||
if (!moveResult && !atLeastOneFailure) pokemon.moveThisTurnResult = null;
|
||
const hitSlot = targets.map(pokemon => pokemon.getSlot());
|
||
if (move.spreadHit) this.attrLastMove('[spread] ' + hitSlot.join(','));
|
||
return moveResult;
|
||
},
|
||
hitStepInvulnerabilityEvent(targets, pokemon, move) {
|
||
if (move.id === 'helpinghand' || (this.gen >= 6 && move.id === 'toxic' && pokemon.hasType('Poison'))) {
|
||
return new Array(targets.length).fill(true);
|
||
}
|
||
const hitResults = this.runEvent('Invulnerability', targets, pokemon, move);
|
||
for (const [i, target] of targets.entries()) {
|
||
if (hitResults[i] === false) {
|
||
if (move.smartTarget) {
|
||
move.smartTarget = false;
|
||
} else {
|
||
if (!move.spreadHit) this.attrLastMove('[miss]');
|
||
this.add('-miss', pokemon, target);
|
||
}
|
||
}
|
||
}
|
||
return hitResults;
|
||
},
|
||
hitStepTryHitEvent(targets, pokemon, move) {
|
||
const hitResults = this.runEvent('TryHit', targets, pokemon, move);
|
||
if (!hitResults.includes(true) && hitResults.includes(false)) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (hitResults[i] !== this.NOT_FAIL) hitResults[i] = hitResults[i] || false;
|
||
}
|
||
return hitResults;
|
||
},
|
||
hitStepTypeImmunity(targets, pokemon, move) {
|
||
if (move.ignoreImmunity === undefined) {
|
||
move.ignoreImmunity = (move.category === 'Status');
|
||
}
|
||
|
||
const hitResults = [];
|
||
for (let i = 0; i < targets.length; i++) {
|
||
hitResults[i] = (move.ignoreImmunity && (move.ignoreImmunity === true || move.ignoreImmunity[move.type])) || targets[i].runImmunity(move.type, !move.smartTarget);
|
||
if (move.smartTarget && !hitResults[i]) move.smartTarget = false;
|
||
}
|
||
|
||
return hitResults;
|
||
},
|
||
hitStepTryImmunity(targets, pokemon, move) {
|
||
const hitResults = [];
|
||
for (let [i, target] of targets.entries()) {
|
||
if (this.gen >= 6 && move.flags['powder'] && target !== pokemon && !this.dex.getImmunity('powder', target)) {
|
||
this.debug('natural powder immunity');
|
||
this.add('-immune', target);
|
||
hitResults[i] = false;
|
||
} else if (!this.singleEvent('TryImmunity', move, {}, target, pokemon, move)) {
|
||
this.add('-immune', target);
|
||
hitResults[i] = false;
|
||
} else if (this.gen >= 7 && move.pranksterBoosted && pokemon.hasAbility('prankster') && targets[i].side !== pokemon.side && !this.dex.getImmunity('prankster', target)) {
|
||
this.debug('natural prankster immunity');
|
||
if (!target.illusion) this.hint("Since gen 7, Dark is immune to Prankster moves.");
|
||
this.add('-immune', target);
|
||
hitResults[i] = false;
|
||
} else {
|
||
hitResults[i] = true;
|
||
}
|
||
}
|
||
return hitResults;
|
||
},
|
||
hitStepAccuracy(targets, pokemon, move) {
|
||
const hitResults = [];
|
||
for (let [i, target] of targets.entries()) {
|
||
// calculate true accuracy
|
||
/** @type {number | true} */ // TypeScript bug: incorrectly infers {number | true} as {number | boolean}
|
||
let accuracy = move.accuracy;
|
||
if (move.ohko) { // bypasses accuracy modifiers
|
||
if (!target.isSemiInvulnerable()) {
|
||
accuracy = 30;
|
||
if (move.ohko === 'Ice' && this.gen >= 7 && !pokemon.hasType('Ice')) {
|
||
accuracy = 20;
|
||
}
|
||
if (!target.volatiles['dynamax'] && pokemon.level >= target.level && (move.ohko === true || !target.hasType(move.ohko))) {
|
||
accuracy += (pokemon.level - target.level);
|
||
} else {
|
||
this.add('-immune', target, '[ohko]');
|
||
hitResults[i] = false;
|
||
continue;
|
||
}
|
||
}
|
||
} else {
|
||
const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3];
|
||
|
||
let boosts, boost;
|
||
if (accuracy !== true) {
|
||
if (!move.ignoreAccuracy) {
|
||
boosts = this.runEvent('ModifyBoost', pokemon, null, null, Object.assign({}, pokemon.boosts));
|
||
boost = this.dex.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.dex.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 || (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.randomChance(accuracy, 100)) {
|
||
if (move.smartTarget) {
|
||
move.smartTarget = false;
|
||
} else {
|
||
if (!move.spreadHit) this.attrLastMove('[miss]');
|
||
this.add('-miss', pokemon, target);
|
||
}
|
||
if (!move.ohko && pokemon.hasItem('blunderpolicy') && pokemon.useItem()) {
|
||
this.boost({spe: 2}, pokemon);
|
||
}
|
||
hitResults[i] = false;
|
||
continue;
|
||
}
|
||
hitResults[i] = true;
|
||
}
|
||
return hitResults;
|
||
},
|
||
hitStepBreakProtect(targets, pokemon, move) {
|
||
if (move.breaksProtect) {
|
||
for (const target of targets) {
|
||
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 (this.gen >= 6) delete target.volatiles['stall'];
|
||
}
|
||
}
|
||
}
|
||
return undefined;
|
||
},
|
||
hitStepStealBoosts(targets, pokemon, move) {
|
||
const target = targets[0]; // hardcoded
|
||
if (move.stealsBoosts) {
|
||
/** @type {{[k: string]: number}} */
|
||
const boosts = {};
|
||
let stolen = false;
|
||
for (const statName in target.boosts) {
|
||
// @ts-ignore
|
||
const 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, pokemon);
|
||
|
||
for (const statName in boosts) {
|
||
boosts[statName] = 0;
|
||
}
|
||
target.setBoost(boosts);
|
||
this.addMove('-anim', pokemon, "Spectral Thief", target);
|
||
}
|
||
}
|
||
return undefined;
|
||
},
|
||
afterMoveSecondaryEvent(targets, pokemon, move) {
|
||
// console.log(`${targets}, ${pokemon}, ${move}`)
|
||
if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) {
|
||
this.singleEvent('AfterMoveSecondary', move, null, targets[0], pokemon, move);
|
||
this.runEvent('AfterMoveSecondary', targets, pokemon, move);
|
||
}
|
||
return undefined;
|
||
},
|
||
/** NOTE: used only for moves that target sides/fields rather than pokemon */
|
||
tryMoveHit(target, pokemon, move) {
|
||
this.setActiveMove(move, pokemon, target);
|
||
|
||
let hitResult = this.singleEvent('PrepareHit', move, {}, target, pokemon, move);
|
||
if (!hitResult) {
|
||
if (hitResult === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
}
|
||
this.runEvent('PrepareHit', pokemon, target, move);
|
||
|
||
if (!this.singleEvent('Try', move, null, pokemon, target, move)) {
|
||
return false;
|
||
}
|
||
|
||
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', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
}
|
||
return this.moveHit(target, pokemon, move);
|
||
},
|
||
hitStepMoveHitLoop(targets, pokemon, move) { // temp name
|
||
/** @type {(number | boolean | undefined)[]} */
|
||
const damage = [];
|
||
for (let i = 0; i < targets.length; i++) damage[i] = 0;
|
||
move.totalDamage = 0;
|
||
pokemon.lastDamage = 0;
|
||
let targetHits = move.multihit || 1;
|
||
if (Array.isArray(targetHits)) {
|
||
// yes, it's hardcoded... meh
|
||
if (targetHits[0] === 2 && targetHits[1] === 5) {
|
||
if (this.gen >= 5) {
|
||
targetHits = this.sample([2, 2, 3, 3, 4, 5]);
|
||
} else {
|
||
targetHits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]);
|
||
}
|
||
} else {
|
||
targetHits = this.random(targetHits[0], targetHits[1] + 1);
|
||
}
|
||
}
|
||
targetHits = Math.floor(targetHits);
|
||
let nullDamage = true;
|
||
/** @type {(number | boolean | undefined)[]} */
|
||
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.dex.getMove(move.sourceEffect).sleepUsable;
|
||
|
||
/** @type {(Pokemon | false | null)[]} */
|
||
let targetsCopy = targets.slice(0);
|
||
let hit;
|
||
for (hit = 1; hit <= targetHits; hit++) {
|
||
if (damage.includes(false)) break;
|
||
if (hit > 1 && pokemon.status === 'slp' && !isSleepUsable) break;
|
||
if (targets.every(target => !target || !target.hp)) break;
|
||
move.hit = hit;
|
||
if (move.smartTarget && targets.length > 1) {
|
||
targetsCopy = [targets[hit - 1]];
|
||
} else {
|
||
targetsCopy = targets.slice(0);
|
||
}
|
||
let target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded
|
||
if (target && typeof move.smartTarget === 'boolean') {
|
||
if (hit > 1) {
|
||
this.addMove('-anim', pokemon, move.name, target);
|
||
} else {
|
||
this.retargetLastMove(target);
|
||
}
|
||
}
|
||
|
||
// like this (Triple Kick)
|
||
if (target && move.multiaccuracy && hit > 1) {
|
||
let accuracy = move.accuracy;
|
||
const boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3];
|
||
if (accuracy !== true) {
|
||
if (!move.ignoreAccuracy) {
|
||
const boosts = this.runEvent('ModifyBoost', pokemon, null, null, Object.assign({}, pokemon.boosts));
|
||
const boost = this.dex.clampIntRange(boosts['accuracy'], -6, 6);
|
||
if (boost > 0) {
|
||
accuracy *= boostTable[boost];
|
||
} else {
|
||
accuracy /= boostTable[-boost];
|
||
}
|
||
}
|
||
if (!move.ignoreEvasion) {
|
||
const boosts = this.runEvent('ModifyBoost', target, null, null, Object.assign({}, target.boosts));
|
||
const boost = this.dex.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.randomChance(accuracy, 100)) break;
|
||
}
|
||
}
|
||
|
||
const moveData = move;
|
||
if (!moveData.flags) moveData.flags = {};
|
||
|
||
// Modifies targetsCopy (which is why it's a copy)
|
||
[moveDamage, targetsCopy] = this.spreadMoveHit(targetsCopy, pokemon, move, moveData);
|
||
|
||
if (!moveDamage.some(val => val !== false)) break;
|
||
nullDamage = false;
|
||
|
||
for (const [i, md] of moveDamage.entries()) {
|
||
// Damage from each hit is individually counted for the
|
||
// purposes of Counter, Metal Burst, and Mirror Coat.
|
||
damage[i] = md === true || !md ? 0 : md;
|
||
// Total damage dealt is accumulated for the purposes of recoil (Parental Bond).
|
||
// @ts-ignore
|
||
move.totalDamage += damage[i];
|
||
}
|
||
if (move.mindBlownRecoil) {
|
||
this.damage(Math.round(pokemon.maxhp / 2), pokemon, pokemon, this.dex.getEffect('Mind Blown'), true);
|
||
move.mindBlownRecoil = false;
|
||
}
|
||
this.eachEvent('Update');
|
||
if (!pokemon.hp && targets.length === 1) {
|
||
hit++; // report the correct number of hits for multihit moves
|
||
break;
|
||
}
|
||
}
|
||
// hit is 1 higher than the actual hit count
|
||
if (hit === 1) return damage.fill(false);
|
||
if (nullDamage) damage.fill(false);
|
||
if (move.multihit && typeof move.smartTarget !== 'boolean') {
|
||
this.add('-hitcount', targets[0], hit - 1);
|
||
}
|
||
|
||
if (move.recoil && move.totalDamage) {
|
||
this.damage(this.calcRecoilDamage(move.totalDamage, move), pokemon, pokemon, 'recoil');
|
||
}
|
||
|
||
if (move.struggleRecoil) {
|
||
// @ts-ignore
|
||
this.directDamage(this.dex.clampIntRange(Math.round(pokemon.maxhp / 4), 1), pokemon, pokemon, {id: 'strugglerecoil'});
|
||
}
|
||
|
||
// smartTarget messes up targetsCopy, but smartTarget should in theory ensure that targets will never fail, anyway
|
||
if (move.smartTarget) targetsCopy = targets.slice(0);
|
||
|
||
for (let i = 0; i < targetsCopy.length; i++) {
|
||
let target = targetsCopy[i];
|
||
if (target && pokemon !== target) {
|
||
// @ts-ignore damage[i] can't be true if target is truthy
|
||
target.gotAttacked(move, damage[i], pokemon);
|
||
}
|
||
}
|
||
|
||
if (move.ohko && !targets[0].hp) this.add('-ohko');
|
||
|
||
if (!damage.some(val => !!val || val === 0)) return damage;
|
||
|
||
this.eachEvent('Update');
|
||
|
||
// @ts-ignore
|
||
this.afterMoveSecondaryEvent(targetsCopy.filter(val => !!val), pokemon, move);
|
||
|
||
if (!move.negateSecondary && !(move.hasSheerForce && pokemon.hasAbility('sheerforce'))) {
|
||
for (let i = 0; i < damage.length; i++) {
|
||
// There are no multihit spread moves, so it's safe to use move.totalDamage for multihit moves
|
||
// The previous check was for `move.multihit`, but that fails for Dragon Darts
|
||
const curDamage = targets.length === 1 ? move.totalDamage : damage[i];
|
||
if (typeof curDamage === 'number' && targets[i].hp) {
|
||
if (targets[i].hp <= targets[i].maxhp / 2 && targets[i].hp + curDamage > targets[i].maxhp / 2) {
|
||
this.runEvent('EmergencyExit', targets[i], pokemon);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return damage;
|
||
},
|
||
spreadMoveHit(targets, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) {
|
||
// Hardcoded for single-target purposes
|
||
// (no spread moves have any kind of onTryHit handler)
|
||
const target = targets[0];
|
||
/** @type {(number | boolean | undefined)[]} */
|
||
let damage = [];
|
||
for (let i = 0; i < targets.length; i++) damage[i] = true;
|
||
const move = this.dex.getActiveMove(moveOrMoveName);
|
||
/** @type {?boolean | number} */
|
||
let hitResult = true;
|
||
if (!moveData) moveData = move;
|
||
if (!moveData.flags) moveData.flags = {};
|
||
if (move.target === 'all' && !isSelf) {
|
||
hitResult = this.singleEvent('TryHitField', moveData, {}, target || null, pokemon, move);
|
||
} else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) {
|
||
hitResult = this.singleEvent('TryHitSide', moveData, {}, (target ? target.side : null), pokemon, move);
|
||
} else if (target) {
|
||
hitResult = this.singleEvent('TryHit', moveData, {}, target, pokemon, move);
|
||
}
|
||
if (!hitResult) {
|
||
if (hitResult === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return [[false], targets]; // single-target only
|
||
}
|
||
|
||
// 0. check for substitute
|
||
if (!isSecondary && !isSelf) {
|
||
if (move.target !== 'all' && move.target !== 'allySide' && move.target !== 'foeSide') {
|
||
damage = this.tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary);
|
||
}
|
||
}
|
||
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (damage[i] === 0) {
|
||
// special substitute flag
|
||
damage[i] = true;
|
||
targets[i] = null;
|
||
}
|
||
if (targets[i] && isSecondary && !moveData.self) {
|
||
damage[i] = true;
|
||
}
|
||
if (!damage[i]) targets[i] = false;
|
||
}
|
||
// 1. call to this.getDamage
|
||
damage = this.getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf);
|
||
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (damage[i] === false) targets[i] = false;
|
||
}
|
||
|
||
// 2. call to this.spreadDamage
|
||
damage = this.spreadDamage(damage, targets, pokemon, move);
|
||
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (!damage && damage !== 0) {
|
||
this.debug('damage interrupted');
|
||
targets[i] = false;
|
||
}
|
||
}
|
||
|
||
// 3. onHit event happens here
|
||
damage = this.runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf);
|
||
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (!damage[i] && damage[i] !== 0) targets[i] = false;
|
||
}
|
||
|
||
// 4. self drops (start checking for targets[i] === false here)
|
||
if (moveData.self && !move.selfDropped) this.selfDrops(targets, pokemon, move, moveData, isSecondary);
|
||
|
||
// 5. secondary effects
|
||
if (moveData.secondaries) this.secondaries(targets, pokemon, move, moveData, isSelf);
|
||
|
||
// 6. force switch
|
||
if (moveData.forceSwitch) damage = this.forceSwitch(damage, targets, pokemon, move, moveData, isSecondary, isSelf);
|
||
|
||
for (let j = 0; j < targets.length; j++) {
|
||
if (!damage[j] && damage[j] !== 0) targets[j] = false;
|
||
}
|
||
|
||
/** @type {Pokemon[]} */
|
||
let damagedTargets = [];
|
||
let damagedDamage = [];
|
||
for (let i = 0; i < targets.length; i++) {
|
||
if (typeof damage[i] === 'number' && targets[i]) {
|
||
damagedTargets.push(/** @type {Pokemon} */ (targets[i]));
|
||
damagedDamage.push(damage[i]);
|
||
}
|
||
}
|
||
const pokemonOriginalHP = pokemon.hp;
|
||
if (damagedDamage.length && !isSecondary && !isSelf) {
|
||
this.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage);
|
||
if (moveData.onAfterHit) {
|
||
for (const target of damagedTargets) {
|
||
this.singleEvent('AfterHit', moveData, {}, target, pokemon, move);
|
||
}
|
||
}
|
||
if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP > pokemon.maxhp / 2) {
|
||
this.runEvent('EmergencyExit', pokemon);
|
||
}
|
||
}
|
||
|
||
return [damage, targets];
|
||
},
|
||
tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary) {
|
||
for (let i = 0; i < targets.length; i++) {
|
||
const target = targets[i];
|
||
if (!target) continue;
|
||
damage[i] = this.runEvent('TryPrimaryHit', target, pokemon, moveData);
|
||
}
|
||
return damage;
|
||
},
|
||
getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf) {
|
||
for (let i = 0; i < targets.length; i++) {
|
||
let target = targets[i];
|
||
if (!target) continue;
|
||
damage[i] = undefined;
|
||
let curDamage = 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 (curDamage === false || curDamage === null) {
|
||
if (damage[i] === false && !isSecondary && !isSelf) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
this.debug('damage calculation interrupted');
|
||
damage[i] = false;
|
||
continue;
|
||
}
|
||
damage[i] = curDamage;
|
||
if (move.selfdestruct === 'ifHit') {
|
||
this.faint(pokemon, pokemon, move);
|
||
}
|
||
if ((damage[i] || damage[i] === 0) && !target.fainted) {
|
||
// @ts-ignore
|
||
if (move.noFaint && damage[i] >= target.hp) {
|
||
damage[i] = target.hp - 1;
|
||
}
|
||
}
|
||
}
|
||
return damage;
|
||
},
|
||
runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf) {
|
||
/**@type {?boolean | number | undefined} */
|
||
let didAnything = damage.reduce(this.combineResults);
|
||
for (const [i, target] of targets.entries()) {
|
||
if (target === false) continue;
|
||
let hitResult;
|
||
/**@type {?boolean | number | undefined} */
|
||
let didSomething = undefined;
|
||
|
||
if (target) {
|
||
if (moveData.boosts && !target.fainted) {
|
||
hitResult = this.boost(moveData.boosts, target, pokemon, move, isSecondary, isSelf);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.heal && !target.fainted) {
|
||
if (target.hp >= target.maxhp) {
|
||
this.add('-fail', pokemon, 'heal');
|
||
this.attrLastMove('[still]');
|
||
damage[i] = this.combineResults(damage[i], false);
|
||
didAnything = this.combineResults(didAnything, null);
|
||
continue;
|
||
}
|
||
let amount = target.baseMaxhp * moveData.heal[0] / moveData.heal[1];
|
||
let d = target.heal((this.gen < 5 ? Math.floor : Math.round)(amount));
|
||
if (!d && d !== 0) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
this.debug('heal interrupted');
|
||
damage[i] = this.combineResults(damage[i], false);
|
||
didAnything = this.combineResults(didAnything, null);
|
||
continue;
|
||
}
|
||
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) {
|
||
damage[i] = this.combineResults(damage[i], false);
|
||
didAnything = this.combineResults(didAnything, null);
|
||
continue;
|
||
}
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.forceStatus) {
|
||
hitResult = target.setStatus(moveData.forceStatus, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.volatileStatus) {
|
||
hitResult = target.addVolatile(moveData.volatileStatus, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.sideCondition) {
|
||
hitResult = target.side.addSideCondition(moveData.sideCondition, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.slotCondition) {
|
||
hitResult = target.side.addSlotCondition(target, moveData.slotCondition, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.weather) {
|
||
hitResult = this.field.setWeather(moveData.weather, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.terrain) {
|
||
hitResult = this.field.setTerrain(moveData.terrain, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.pseudoWeather) {
|
||
hitResult = this.field.addPseudoWeather(moveData.pseudoWeather, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (moveData.forceSwitch) {
|
||
hitResult = !!this.canSwitch(target.side);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
// 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. ;)
|
||
if (move.target === 'all' && !isSelf) {
|
||
if (moveData.onHitField) {
|
||
hitResult = this.singleEvent('HitField', moveData, {}, target, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
} else if ((move.target === 'foeSide' || move.target === 'allySide') && !isSelf) {
|
||
if (moveData.onHitSide) {
|
||
hitResult = this.singleEvent('HitSide', moveData, {}, target.side, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
} else {
|
||
if (moveData.onHit) {
|
||
hitResult = this.singleEvent('Hit', moveData, {}, target, pokemon, move);
|
||
didSomething = this.combineResults(didSomething, hitResult);
|
||
}
|
||
if (!isSelf && !isSecondary) {
|
||
this.runEvent('Hit', target, pokemon, move);
|
||
}
|
||
}
|
||
}
|
||
if (moveData.selfSwitch) {
|
||
if (this.canSwitch(pokemon.side)) {
|
||
didSomething = true;
|
||
} else {
|
||
didSomething = this.combineResults(didSomething, false);
|
||
}
|
||
}
|
||
// Move didn't fail because it didn't try to do anything
|
||
if (didSomething === undefined) didSomething = true;
|
||
damage[i] = this.combineResults(damage[i], didSomething === null ? false : didSomething);
|
||
didAnything = this.combineResults(didAnything, didSomething);
|
||
}
|
||
|
||
|
||
if (!didAnything && didAnything !== 0 && !moveData.self && !moveData.selfdestruct) {
|
||
if (!isSelf && !isSecondary) {
|
||
if (didAnything === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
}
|
||
this.debug('move failed because it did nothing');
|
||
} else if (move.selfSwitch && pokemon.hp) {
|
||
pokemon.switchFlag = move.id;
|
||
}
|
||
|
||
return damage;
|
||
},
|
||
selfDrops(targets, pokemon, move, moveData, isSecondary) {
|
||
for (let i = 0; i < targets.length; i++) {
|
||
let target = targets[i];
|
||
if (target === false) continue;
|
||
if (moveData.self && !move.selfDropped) {
|
||
let selfRoll = 0;
|
||
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 (moveData.self.chance === undefined || selfRoll < moveData.self.chance) {
|
||
this.moveHit(pokemon, pokemon, move, moveData.self, isSecondary, true);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
secondaries(targets, pokemon, move, moveData, isSelf) {
|
||
if (!moveData.secondaries) return;
|
||
for (const target of targets) {
|
||
if (target === false) continue;
|
||
/** @type {SecondaryEffect[]} */
|
||
let secondaries = this.runEvent('ModifySecondaries', target, pokemon, moveData, moveData.secondaries.slice());
|
||
for (const secondary of secondaries) {
|
||
let secondaryRoll = this.random(100);
|
||
if (typeof secondary.chance === 'undefined' || secondaryRoll < secondary.chance) {
|
||
this.moveHit(target, pokemon, move, secondary, true, isSelf);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
forceSwitch(damage, targets, pokemon, move) {
|
||
for (const [i, target] of targets.entries()) {
|
||
if (target && target.hp > 0 && pokemon.hp > 0 && this.canSwitch(target.side)) {
|
||
let hitResult = this.runEvent('DragOut', target, pokemon, move);
|
||
if (hitResult) {
|
||
target.forceSwitchFlag = true;
|
||
} else if (hitResult === false && move.category === 'Status') {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
damage[i] = false;
|
||
}
|
||
}
|
||
}
|
||
return damage;
|
||
},
|
||
moveHit(target, pokemon, moveOrMoveName, moveData, isSecondary, isSelf) {
|
||
let retVal = this.spreadMoveHit([target], pokemon, moveOrMoveName, moveData, isSecondary, isSelf)[0][0];
|
||
return retVal === true ? undefined : retVal;
|
||
},
|
||
|
||
calcRecoilDamage(damageDealt, move) {
|
||
// @ts-ignore
|
||
return this.dex.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(move, pokemon, skipChecks) {
|
||
let item = pokemon.getItem();
|
||
if (!skipChecks) {
|
||
if (pokemon.side.zMoveUsed) return;
|
||
if (!item.zMove) return;
|
||
if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) 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 /** @type {string} */ (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];
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
getActiveZMove(move, pokemon) {
|
||
if (pokemon) {
|
||
let item = pokemon.getItem();
|
||
if (move.name === item.zMoveFrom) {
|
||
// @ts-ignore
|
||
let zMove = this.dex.getActiveMove(item.zMove);
|
||
zMove.isZPowered = true;
|
||
return zMove;
|
||
}
|
||
}
|
||
|
||
if (move.category === 'Status') {
|
||
let zMove = this.dex.getActiveMove(move);
|
||
zMove.isZ = true;
|
||
zMove.isZPowered = true;
|
||
return zMove;
|
||
}
|
||
let zMove = this.dex.getActiveMove(this.zMoveTable[move.type]);
|
||
// @ts-ignore
|
||
zMove.basePower = move.zMovePower;
|
||
zMove.category = move.category;
|
||
// copy the priority for Quick Guard
|
||
zMove.priority = move.priority;
|
||
zMove.isZPowered = true;
|
||
return zMove;
|
||
},
|
||
|
||
canZMove(pokemon) {
|
||
if (pokemon.side.zMoveUsed || (pokemon.transformed && (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra"))) return;
|
||
let item = pokemon.getItem();
|
||
if (!item.zMove) return;
|
||
if (item.itemUser && !item.itemUser.includes(pokemon.species.name)) return;
|
||
let atLeastOne = false;
|
||
let mustStruggle = true;
|
||
/**@type {ZMoveOptions} */
|
||
let zMoves = [];
|
||
for (const moveSlot of pokemon.moveSlots) {
|
||
if (moveSlot.pp <= 0) {
|
||
zMoves.push(null);
|
||
continue;
|
||
}
|
||
if (!moveSlot.disabled) {
|
||
mustStruggle = false;
|
||
}
|
||
let move = this.dex.getMove(moveSlot.move);
|
||
let zMoveName = this.getZMove(move, pokemon, true) || '';
|
||
if (zMoveName) {
|
||
let 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;
|
||
},
|
||
|
||
canMegaEvo(pokemon) {
|
||
let altForme = pokemon.baseSpecies.otherFormes && this.dex.getSpecies(pokemon.baseSpecies.otherFormes[0]);
|
||
let item = pokemon.getItem();
|
||
if (altForme && altForme.isMega && altForme.requiredMove && pokemon.baseMoves.includes(toID(altForme.requiredMove)) && !item.zMove) return altForme.name;
|
||
if (item.megaEvolves !== pokemon.baseSpecies.baseSpecies || item.megaStone === pokemon.name) {
|
||
return null;
|
||
}
|
||
return item.megaStone;
|
||
},
|
||
|
||
canUltraBurst(pokemon) {
|
||
if (['Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane'].includes(pokemon.baseSpecies.name) &&
|
||
pokemon.getItem().id === 'ultranecroziumz') {
|
||
return "Necrozma-Ultra";
|
||
}
|
||
return null;
|
||
},
|
||
|
||
maxMoveTable: {
|
||
Flying: 'Max Airstream',
|
||
Dark: 'Max Darkness',
|
||
Fire: 'Max Flare',
|
||
Bug: 'Max Flutterby',
|
||
Water: 'Max Geyser',
|
||
Status: 'Max Guard',
|
||
Ice: 'Max Hailstorm',
|
||
Fighting: 'Max Knuckle',
|
||
Electric: 'Max Lightning',
|
||
Psychic: 'Max Mindstorm',
|
||
Poison: 'Max Ooze',
|
||
Grass: 'Max Overgrowth',
|
||
Ghost: 'Max Phantasm',
|
||
Ground: 'Max Quake',
|
||
Rock: 'Max Rockfall',
|
||
Fairy: 'Max Starfall',
|
||
Steel: 'Max Steelspike',
|
||
Normal: 'Max Strike',
|
||
Dragon: 'Max Wyrmwind',
|
||
},
|
||
|
||
canDynamax(pokemon, skipChecks) {
|
||
// {gigantamax?: string, maxMoves: {[k: string]: string} | null}[]
|
||
if (!skipChecks) {
|
||
if (!pokemon.canDynamax) return;
|
||
if (pokemon.species.isMega || pokemon.species.isPrimal || pokemon.species.forme === "Ultra" || pokemon.getItem().zMove || this.canMegaEvo(pokemon)) {
|
||
return;
|
||
}
|
||
// Some pokemon species are unable to dynamax
|
||
const cannotDynamax = ['zacian', 'zamazenta', 'eternatus'];
|
||
if (cannotDynamax.includes(toID(pokemon.species.baseSpecies))) {
|
||
return;
|
||
}
|
||
}
|
||
/** @type {DynamaxOptions} */
|
||
let result = {maxMoves: []};
|
||
for (let moveSlot of pokemon.moveSlots) {
|
||
let move = this.dex.getMove(moveSlot.id);
|
||
let maxMove = this.getMaxMove(move, pokemon);
|
||
if (maxMove) {
|
||
if (pokemon.maxMoveDisabled(maxMove)) {
|
||
result.maxMoves.push({move: maxMove.id, target: maxMove.target, disabled: true});
|
||
} else {
|
||
result.maxMoves.push({move: maxMove.id, target: maxMove.target});
|
||
}
|
||
}
|
||
}
|
||
if (pokemon.canGigantamax) result.gigantamax = pokemon.canGigantamax;
|
||
return result;
|
||
},
|
||
|
||
getMaxMove(move, pokemon) {
|
||
if (typeof move === 'string') move = this.dex.getMove(move);
|
||
if (pokemon.canGigantamax && move.category !== 'Status') {
|
||
let gMaxSpecies = this.dex.getSpecies(pokemon.canGigantamax);
|
||
let gMaxMove = this.dex.getMove(gMaxSpecies.isGigantamax);
|
||
if (gMaxMove.exists && gMaxMove.type === move.type) return gMaxMove;
|
||
}
|
||
let maxMove = this.dex.getMove(this.maxMoveTable[move.category === 'Status' ? move.category : move.type]);
|
||
if (maxMove.exists) return maxMove;
|
||
},
|
||
|
||
getActiveMaxMove(move, pokemon) {
|
||
if (typeof move === 'string') move = this.dex.getActiveMove(move);
|
||
let maxMove = this.dex.getActiveMove(this.maxMoveTable[move.category === 'Status' ? move.category : move.type]);
|
||
if (move.category !== 'Status') {
|
||
if (pokemon.canGigantamax) {
|
||
let gMaxSpecies = this.dex.getSpecies(pokemon.canGigantamax);
|
||
let gMaxMove = this.dex.getActiveMove(gMaxSpecies.isGigantamax ? gMaxSpecies.isGigantamax : '');
|
||
if (gMaxMove.exists && gMaxMove.type === move.type) maxMove = gMaxMove;
|
||
}
|
||
if (!move.gmaxPower) throw new Error(`${move.name} doesn't have a gmaxPower`);
|
||
maxMove.basePower = move.gmaxPower;
|
||
maxMove.category = move.category;
|
||
}
|
||
maxMove.baseMove = move.id;
|
||
maxMove.maxPowered = true;
|
||
return maxMove;
|
||
},
|
||
|
||
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 one mega evolution
|
||
let wasMega = pokemon.canMegaEvo;
|
||
for (const ally of side.pokemon) {
|
||
if (wasMega) {
|
||
ally.canMegaEvo = null;
|
||
} else {
|
||
ally.canUltraBurst = null;
|
||
}
|
||
}
|
||
|
||
this.runEvent('AfterMega', pokemon);
|
||
return true;
|
||
},
|
||
|
||
runZPower(move, pokemon) {
|
||
const zPower = this.dex.getEffect('zpower');
|
||
if (move.category !== 'Status') {
|
||
this.attrLastMove('[zeffect]');
|
||
} else if (move.zMoveBoost) {
|
||
this.boost(move.zMoveBoost, pokemon, pokemon, zPower);
|
||
} else {
|
||
switch (move.zMoveEffect) {
|
||
case 'heal':
|
||
this.heal(pokemon.maxhp, pokemon, pokemon, zPower);
|
||
break;
|
||
case 'healreplacement':
|
||
move.self = {slotCondition: 'healreplacement'};
|
||
break;
|
||
case 'clearnegativeboost':
|
||
/** @type {{[k: string]: number}} */
|
||
let boosts = {};
|
||
for (let i in pokemon.boosts) {
|
||
// @ts-ignore
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
isAdjacent(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(targetType) {
|
||
return CHOOSABLE_TARGETS.has(targetType);
|
||
},
|
||
};
|
||
|
||
exports.BattleScripts = BattleScripts;
|