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