mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-17 18:51:43 -05:00
In certain cases, a spread move that KOed an opponent would send an incorrect message which could crash the client. This fixes that while also simplifying the code.
383 lines
12 KiB
JavaScript
383 lines
12 KiB
JavaScript
'use strict';
|
||
|
||
/**@type {ModdedBattleScriptsData} */
|
||
let BattleScripts = {
|
||
inherit: 'gen4',
|
||
gen: 3,
|
||
init() {
|
||
for (let i in this.data.Pokedex) {
|
||
delete this.data.Pokedex[i].abilities['H'];
|
||
}
|
||
let specialTypes = ['Fire', 'Water', 'Grass', 'Ice', 'Electric', 'Dark', 'Psychic', 'Dragon'];
|
||
let newCategory = '';
|
||
for (let i in this.data.Movedex) {
|
||
if (!this.data.Movedex[i]) console.log(i);
|
||
if (this.data.Movedex[i].category === 'Status') continue;
|
||
newCategory = specialTypes.includes(this.data.Movedex[i].type) ? 'Special' : 'Physical';
|
||
if (newCategory !== this.data.Movedex[i].category) {
|
||
this.modData('Movedex', i).category = newCategory;
|
||
}
|
||
}
|
||
},
|
||
useMoveInner(moveOrMoveName, pokemon, target, sourceEffect, zMove) {
|
||
if (!sourceEffect && this.effect.id) sourceEffect = this.effect;
|
||
if (sourceEffect && sourceEffect.id === 'instruct') sourceEffect = null;
|
||
|
||
let move = this.getActiveMove(moveOrMoveName);
|
||
|
||
if (this.activeMove) {
|
||
move.priority = this.activeMove.priority;
|
||
}
|
||
const baseTarget = move.target;
|
||
if (target === undefined) target = this.resolveTarget(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('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 || pokemon.fainted) {
|
||
return false;
|
||
}
|
||
|
||
let attrs = '';
|
||
|
||
let movename = move.name;
|
||
if (move.id === 'hiddenpower') movename = 'Hidden Power';
|
||
if (sourceEffect) attrs += `|[from]${this.getEffect(sourceEffect)}`;
|
||
this.addMove('move', pokemon, movename, target + attrs);
|
||
|
||
if (!target) {
|
||
this.attrLastMove('[notarget]');
|
||
this.add('-notarget', pokemon);
|
||
return false;
|
||
}
|
||
|
||
const {targets, pressureTargets} = pokemon.getMoveTargets(move, target);
|
||
|
||
if (!sourceEffect || sourceEffect.id === 'pursuit') {
|
||
let extraPP = 0;
|
||
for (const source of pressureTargets) {
|
||
const 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 (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 (move.target === 'allAdjacent' || move.target === 'allAdjacentFoes') {
|
||
if (!targets.length) {
|
||
this.attrLastMove('[notarget]');
|
||
this.add('-notarget', pokemon);
|
||
return false;
|
||
}
|
||
if (targets.length > 1) move.spreadHit = true;
|
||
const hitSlots = [];
|
||
for (const source of targets) {
|
||
const hitResult = this.tryMoveHit(source, pokemon, move);
|
||
if (hitResult || hitResult === 0 || hitResult === undefined) {
|
||
moveResult = true;
|
||
hitSlots.push(source.getSlot());
|
||
}
|
||
if (damage) {
|
||
damage += hitResult || 0;
|
||
} else {
|
||
if (damage !== false || hitResult !== this.NOT_FAIL) damage = hitResult;
|
||
}
|
||
if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null;
|
||
}
|
||
if (move.spreadHit) this.attrLastMove('[spread] ' + hitSlots.join(','));
|
||
} else {
|
||
target = targets[0];
|
||
let lacksTarget = !target || 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.isFutureMove) {
|
||
this.attrLastMove('[notarget]');
|
||
this.add('-notarget', pokemon);
|
||
return false;
|
||
}
|
||
damage = this.tryMoveHit(target, pokemon, move);
|
||
if (damage === this.NOT_FAIL) pokemon.moveThisTurnResult = null;
|
||
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(target, pokemon, move) {
|
||
this.setActiveMove(move, pokemon, target);
|
||
let naturalImmunity = false;
|
||
let accPass = true;
|
||
|
||
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' || 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', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
}
|
||
return this.moveHit(target, pokemon, move);
|
||
}
|
||
|
||
hitResult = this.runEvent('TryImmunity', target, pokemon, move);
|
||
if (!hitResult) {
|
||
if (!move.spreadHit) this.attrLastMove('[miss]');
|
||
this.add('-miss', pokemon, target);
|
||
return false;
|
||
}
|
||
|
||
if (move.ignoreImmunity === undefined) {
|
||
move.ignoreImmunity = (move.category === 'Status');
|
||
}
|
||
|
||
if ((!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && !target.runImmunity(move.type)) {
|
||
naturalImmunity = true;
|
||
}
|
||
|
||
let boostTable = [1, 4 / 3, 5 / 3, 2, 7 / 3, 8 / 3, 3];
|
||
|
||
// calculate true accuracy
|
||
/**@type {number | true} */
|
||
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 (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) {
|
||
accuracy = true; // bypasses ohko accuracy modifiers
|
||
} else {
|
||
accuracy = this.runEvent('Accuracy', target, pokemon, move, accuracy);
|
||
}
|
||
if (accuracy !== true && !this.randomChance(accuracy, 100)) {
|
||
accPass = false;
|
||
}
|
||
|
||
if (accPass) {
|
||
hitResult = this.runEvent('TryHit', target, pokemon, move);
|
||
if (!hitResult) {
|
||
if (hitResult === false) {
|
||
this.add('-fail', pokemon);
|
||
this.attrLastMove('[still]');
|
||
}
|
||
return false;
|
||
} else if (naturalImmunity) {
|
||
this.add('-immune', target);
|
||
return false;
|
||
}
|
||
} else {
|
||
if (naturalImmunity) {
|
||
this.add('-immune', target);
|
||
} else {
|
||
if (!move.spreadHit) this.attrLastMove('[miss]');
|
||
this.add('-miss', pokemon, target);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
move.totalDamage = 0;
|
||
/** @type {number | undefined | false} */
|
||
let damage = 0;
|
||
pokemon.lastDamage = 0;
|
||
if (move.multihit) {
|
||
let hits = move.multihit;
|
||
if (Array.isArray(hits)) {
|
||
// yes, it's hardcoded... meh
|
||
if (hits[0] === 2 && hits[1] === 5) {
|
||
hits = this.sample([2, 2, 2, 3, 3, 3, 4, 5]);
|
||
} else {
|
||
hits = this.random(hits[0], hits[1] + 1);
|
||
}
|
||
}
|
||
hits = Math.floor(hits);
|
||
let nullDamage = true;
|
||
/** @type {number | undefined | false} */
|
||
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;
|
||
move.hit = i + 1;
|
||
|
||
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.randomChance(accuracy, 100)) 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);
|
||
move.totalDamage += damage;
|
||
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 (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) {
|
||
this.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move);
|
||
this.runEvent('AfterMoveSecondary', target, pokemon, move);
|
||
}
|
||
|
||
return damage;
|
||
},
|
||
|
||
calcRecoilDamage(damageDealt, move) {
|
||
// @ts-ignore
|
||
return this.clampIntRange(Math.floor(damageDealt * move.recoil[0] / move.recoil[1]), 1);
|
||
},
|
||
};
|
||
|
||
exports.BattleScripts = BattleScripts;
|