mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
This is a really hacky implementation of Emergency Exit, but Emergency Exit itself is a huge mess on cart, too. Our previous implementation: - activated Emergency Exit at AfterMoveSecondary timing for move damage - activated Emergency Exit immediately after dealing any other damage This new one: - activates Emergency Exit only in three situations: - right after AfterMoveSecondary timing, for move damage - right after DamagingHit timing, for DamagingHit residual damage (Rough Skin, Iron Barbs, Rocky Helmet) - right after the switch update, for switch-hazard residual damage (Stealth Rock, Spikes) - does not otherwise activate (so Substitute, Hail, Toxic, etc no longer activate Emergency Exit) This should much accurately simulate Emergency Exit behavior, including most famously timing it after healing berries after hazards, as documented in: https://www.smogon.com/forums/threads/pokemon-sun-moon-battle-mechanics-research.3586701/#post-7075354 Fixes #6309
1342 lines
47 KiB
JavaScript
1342 lines
47 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) {
|
||
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.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);
|
||
// Using a Dancer move is enough to spoil Fake Out etc.
|
||
dancer.activeTurns++;
|
||
}
|
||
}
|
||
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.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.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, true);
|
||
}
|
||
|
||
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.spreadHit) this.attrLastMove('[miss]');
|
||
this.add('-miss', pokemon, target);
|
||
if (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.some(target => target && !target.hp)) break;
|
||
move.hit = hit;
|
||
targetsCopy = targets.slice(0);
|
||
let target = targetsCopy[0]; // some relevant-to-single-target-moves-only things are hardcoded
|
||
|
||
// 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) {
|
||
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) 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'});
|
||
}
|
||
|
||
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++) {
|
||
const curDamage = damage[i];
|
||
if (typeof curDamage === 'number' && 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') {
|
||
damagedTargets.push(/** @type {Pokemon} */ (targets[i]));
|
||
damagedDamage.push(damage[i]);
|
||
}
|
||
}
|
||
const pokemonOriginalHP = pokemon.hp;
|
||
if (damagedDamage.length) {
|
||
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.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 the move is Parting Shot and it fails to change the target's stats in gen 7, didSomething will be null instead of undefined.
|
||
// Leaving didSomething as null will cause this function to return without setting the switch flag, preventing the switch.
|
||
if (this.canSwitch(pokemon.side) && (didSomething !== null || this.gen < 7)) {
|
||
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.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 /** @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.template.isMega || pokemon.template.isPrimal || pokemon.template.forme === "Ultra"))) return;
|
||
let item = pokemon.getItem();
|
||
if (!item.zMove) return;
|
||
if (item.itemUser && !item.itemUser.includes(pokemon.template.species)) 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.baseTemplate.otherFormes && this.dex.getTemplate(pokemon.baseTemplate.otherFormes[0]);
|
||
let item = pokemon.getItem();
|
||
if (altForme && altForme.isMega && altForme.requiredMove && pokemon.baseMoves.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(pokemon) {
|
||
if (['Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane'].includes(pokemon.baseTemplate.species) &&
|
||
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.template.isMega || pokemon.template.isPrimal || pokemon.template.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.template.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) 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 gMaxTemplate = this.dex.getTemplate(pokemon.canGigantamax);
|
||
let gMaxMove = this.dex.getMove(gMaxTemplate.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 gMaxTemplate = this.dex.getTemplate(pokemon.canGigantamax);
|
||
let gMaxMove = this.dex.getActiveMove(gMaxTemplate.isGigantamax ? gMaxTemplate.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 templateid = pokemon.canMegaEvo || pokemon.canUltraBurst;
|
||
if (!templateid) 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(templateid, 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;
|