mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-25 15:40:31 -05:00
4780 lines
154 KiB
JavaScript
4780 lines
154 KiB
JavaScript
/**
|
|
* Simulator process
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* This file is where the battle simulation itself happens.
|
|
*
|
|
* The most important part of the simulation happens in runEvent -
|
|
* see that function's definition for details.
|
|
*
|
|
* @license MIT license
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
require('sugar');
|
|
if (!''.includes) require('es6-shim');
|
|
|
|
global.Config = require('./config/config.js');
|
|
|
|
if (Config.crashguard) {
|
|
// graceful crash - allow current battles to finish before restarting
|
|
process.on('uncaughtException', function (err) {
|
|
require('./crashlogger.js')(err, 'A simulator process', true);
|
|
/* let stack = ("" + err.stack).escapeHTML().split("\n").slice(0, 2).join("<br />");
|
|
if (Rooms.lobby) {
|
|
Rooms.lobby.addRaw('<div><b>THE SERVER HAS CRASHED:</b> ' + stack + '<br />Please restart the server.</div>');
|
|
Rooms.lobby.addRaw('<div>You will not be able to talk in the lobby or start new battles until the server restarts.</div>');
|
|
}
|
|
Rooms.global.lockdown = true; */
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Converts anything to an ID. An ID must have only lowercase alphanumeric
|
|
* characters.
|
|
* If a string is passed, it will be converted to lowercase and
|
|
* non-alphanumeric characters will be stripped.
|
|
* If an object with an ID is passed, its ID will be returned.
|
|
* Otherwise, an empty string will be returned.
|
|
*/
|
|
global.toId = function (text) {
|
|
if (text && text.id) {
|
|
text = text.id;
|
|
} else if (text && text.userid) {
|
|
text = text.userid;
|
|
}
|
|
|
|
if (typeof text !== 'string' && typeof text !== 'number') return '';
|
|
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
};
|
|
|
|
global.Tools = require('./tools.js').includeMods();
|
|
|
|
let Battle, BattleSide, BattlePokemon;
|
|
|
|
let Battles = Object.create(null);
|
|
|
|
require('./repl.js').start('battle-engine-', process.pid, function (cmd) { return eval(cmd); });
|
|
|
|
// Receive and process a message sent using Simulator.prototype.send in
|
|
// another process.
|
|
process.on('message', function (message) {
|
|
//console.log('CHILD MESSAGE RECV: "' + message + '"');
|
|
let nlIndex = message.indexOf("\n");
|
|
let more = '';
|
|
if (nlIndex > 0) {
|
|
more = message.substr(nlIndex + 1);
|
|
message = message.substr(0, nlIndex);
|
|
}
|
|
let data = message.split('|');
|
|
if (data[1] === 'init') {
|
|
if (!Battles[data[0]]) {
|
|
try {
|
|
Battles[data[0]] = Battle.construct(data[0], data[2], data[3]);
|
|
} catch (err) {
|
|
let stack = err.stack + '\n\n' +
|
|
'Additional information:\n' +
|
|
'message = ' + message;
|
|
let fakeErr = {stack: stack};
|
|
|
|
if (!require('./crashlogger.js')(fakeErr, 'A battle')) {
|
|
let ministack = ("" + err.stack).escapeHTML().split("\n").slice(0, 2).join("<br />");
|
|
process.send(data[0] + '\nupdate\n|html|<div class="broadcast-red"><b>A BATTLE PROCESS HAS CRASHED:</b> ' + ministack + '</div>');
|
|
} else {
|
|
process.send(data[0] + '\nupdate\n|html|<div class="broadcast-red"><b>The battle crashed!</b><br />Don\'t worry, we\'re working on fixing it.</div>');
|
|
}
|
|
}
|
|
}
|
|
} else if (data[1] === 'dealloc') {
|
|
if (Battles[data[0]] && Battles[data[0]].destroy) {
|
|
Battles[data[0]].destroy();
|
|
} else {
|
|
let stack = '\n\n' +
|
|
'Additional information:\n' +
|
|
'message = ' + message;
|
|
let fakeErr = {stack: stack};
|
|
|
|
require('./crashlogger.js')(fakeErr, 'A battle');
|
|
}
|
|
delete Battles[data[0]];
|
|
} else {
|
|
let battle = Battles[data[0]];
|
|
if (battle) {
|
|
let prevRequest = battle.currentRequest;
|
|
let prevRequestDetails = battle.currentRequestDetails || '';
|
|
try {
|
|
battle.receive(data, more);
|
|
} catch (err) {
|
|
let stack = err.stack + '\n\n' +
|
|
'Additional information:\n' +
|
|
'message = ' + message + '\n' +
|
|
'currentRequest = ' + prevRequest + '\n\n' +
|
|
'Log:\n' + battle.log.join('\n').replace(/\n\|split\n[^\n]*\n[^\n]*\n[^\n]*\n/g, '\n');
|
|
let fakeErr = {stack: stack};
|
|
require('./crashlogger.js')(fakeErr, 'A battle');
|
|
|
|
let logPos = battle.log.length;
|
|
battle.add('html', '<div class="broadcast-red"><b>The battle crashed</b><br />You can keep playing but it might crash again.</div>');
|
|
let nestedError;
|
|
try {
|
|
battle.makeRequest(prevRequest, prevRequestDetails);
|
|
} catch (e) {
|
|
nestedError = e;
|
|
}
|
|
battle.sendUpdates(logPos);
|
|
if (nestedError) {
|
|
throw nestedError;
|
|
}
|
|
}
|
|
} else if (data[1] === 'eval') {
|
|
try {
|
|
eval(data[2]);
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
});
|
|
|
|
process.on('disconnect', function () {
|
|
process.exit();
|
|
});
|
|
|
|
BattlePokemon = (function () {
|
|
function BattlePokemon(set, side) {
|
|
this.side = side;
|
|
this.battle = side.battle;
|
|
|
|
let pokemonScripts = this.battle.data.Scripts.pokemon;
|
|
if (pokemonScripts) Object.merge(this, pokemonScripts);
|
|
|
|
if (typeof set === 'string') set = {name: set};
|
|
|
|
// "pre-bound" functions for nicer syntax (avoids repeated use of `bind`)
|
|
this.getHealth = (this.getHealth || BattlePokemon.getHealth).bind(this);
|
|
this.getDetails = (this.getDetails || BattlePokemon.getDetails).bind(this);
|
|
|
|
this.set = set;
|
|
|
|
this.baseTemplate = this.battle.getTemplate(set.species || set.name);
|
|
if (!this.baseTemplate.exists) {
|
|
this.battle.debug('Unidentified species: ' + this.species);
|
|
this.baseTemplate = this.battle.getTemplate('Unown');
|
|
}
|
|
this.species = this.baseTemplate.species;
|
|
if (set.name === set.species || !set.name || !set.species) {
|
|
set.name = this.species;
|
|
}
|
|
this.name = (set.name || set.species || 'Bulbasaur').substr(0, 20);
|
|
this.speciesid = toId(this.species);
|
|
this.template = this.baseTemplate;
|
|
this.moves = [];
|
|
this.baseMoves = this.moves;
|
|
this.movepp = {};
|
|
this.moveset = [];
|
|
this.baseMoveset = [];
|
|
|
|
this.level = this.battle.clampIntRange(set.forcedLevel || set.level || 100, 1, 9999);
|
|
|
|
let genders = {M:'M', F:'F'};
|
|
this.gender = this.template.gender || genders[set.gender] || (Math.random() * 2 < 1 ? 'M' : 'F');
|
|
if (this.gender === 'N') this.gender = '';
|
|
this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255;
|
|
this.pokeball = this.set.pokeball || 'pokeball';
|
|
|
|
this.fullname = this.side.id + ': ' + this.name;
|
|
this.details = this.species + (this.level === 100 ? '' : ', L' + this.level) + (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : '');
|
|
|
|
this.id = this.fullname; // shouldn't really be used anywhere
|
|
|
|
this.statusData = {};
|
|
this.volatiles = {};
|
|
|
|
this.height = this.template.height;
|
|
this.heightm = this.template.heightm;
|
|
this.weight = this.template.weight;
|
|
this.weightkg = this.template.weightkg;
|
|
|
|
this.baseAbility = toId(set.ability);
|
|
this.ability = this.baseAbility;
|
|
this.item = toId(set.item);
|
|
this.abilityData = {id: this.ability};
|
|
this.itemData = {id: this.item};
|
|
this.speciesData = {id: this.speciesid};
|
|
|
|
this.types = this.baseTemplate.types;
|
|
this.typesData = [];
|
|
|
|
for (let i = 0, l = this.types.length; i < l; i++) {
|
|
this.typesData.push({
|
|
type: this.types[i],
|
|
suppressed: false,
|
|
isAdded: false
|
|
});
|
|
}
|
|
|
|
if (this.set.moves) {
|
|
for (let i = 0; i < this.set.moves.length; i++) {
|
|
let move = this.battle.getMove(this.set.moves[i]);
|
|
if (!move.id) continue;
|
|
if (move.id === 'hiddenpower') {
|
|
if (!this.set.ivs || Object.values(this.set.ivs).every(31)) {
|
|
this.set.ivs = this.battle.getType(move.type).HPivs;
|
|
}
|
|
move = this.battle.getMove('hiddenpower');
|
|
}
|
|
this.baseMoveset.push({
|
|
move: move.name,
|
|
id: move.id,
|
|
pp: (move.noPPBoosts ? move.pp : move.pp * 8 / 5),
|
|
maxpp: (move.noPPBoosts ? move.pp : move.pp * 8 / 5),
|
|
target: (move.nonGhostTarget && !this.hasType('Ghost') ? move.nonGhostTarget : move.target),
|
|
disabled: false,
|
|
used: false
|
|
});
|
|
this.moves.push(move.id);
|
|
}
|
|
}
|
|
this.disabledMoves = {};
|
|
|
|
this.canMegaEvo = this.battle.canMegaEvo(this);
|
|
|
|
if (!this.set.evs) {
|
|
this.set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0};
|
|
}
|
|
if (!this.set.ivs) {
|
|
this.set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31};
|
|
}
|
|
let stats = {hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31};
|
|
for (let i in stats) {
|
|
if (!this.set.evs[i]) this.set.evs[i] = 0;
|
|
if (!this.set.ivs[i] && this.set.ivs[i] !== 0) this.set.ivs[i] = 31;
|
|
}
|
|
for (let i in this.set.evs) {
|
|
this.set.evs[i] = this.battle.clampIntRange(this.set.evs[i], 0, 255);
|
|
}
|
|
for (let i in this.set.ivs) {
|
|
this.set.ivs[i] = this.battle.clampIntRange(this.set.ivs[i], 0, 31);
|
|
}
|
|
|
|
let hpTypes = ['Fighting', 'Flying', 'Poison', 'Ground', 'Rock', 'Bug', 'Ghost', 'Steel', 'Fire', 'Water', 'Grass', 'Electric', 'Psychic', 'Ice', 'Dragon', 'Dark'];
|
|
if (this.battle.gen && this.battle.gen === 2) {
|
|
// Gen 2 specific Hidden Power check. IVs are still treated 0-31 so we get them 0-15
|
|
let atkDV = Math.floor(this.set.ivs.atk / 2);
|
|
let defDV = Math.floor(this.set.ivs.def / 2);
|
|
let speDV = Math.floor(this.set.ivs.spe / 2);
|
|
let spcDV = Math.floor(this.set.ivs.spa / 2);
|
|
this.hpType = hpTypes[4 * (atkDV % 4) + (defDV % 4)];
|
|
this.hpPower = Math.floor((5 * ((spcDV >> 3) + (2 * (speDV >> 3)) + (4 * (defDV >> 3)) + (8 * (atkDV >> 3))) + (spcDV > 2 ? 3 : spcDV)) / 2 + 31);
|
|
} else {
|
|
// Hidden Power check for gen 3 onwards
|
|
let hpTypeX = 0, hpPowerX = 0;
|
|
let i = 1;
|
|
for (let s in stats) {
|
|
hpTypeX += i * (this.set.ivs[s] % 2);
|
|
hpPowerX += i * (Math.floor(this.set.ivs[s] / 2) % 2);
|
|
i *= 2;
|
|
}
|
|
this.hpType = hpTypes[Math.floor(hpTypeX * 15 / 63)];
|
|
// In Gen 6, Hidden Power is always 60 base power
|
|
this.hpPower = (this.battle.gen && this.battle.gen < 6) ? Math.floor(hpPowerX * 40 / 63) + 30 : 60;
|
|
}
|
|
|
|
this.boosts = {atk: 0, def: 0, spa: 0, spd: 0, spe: 0, accuracy: 0, evasion: 0};
|
|
this.stats = {atk:0, def:0, spa:0, spd:0, spe:0};
|
|
this.baseStats = {atk:10, def:10, spa:10, spd:10, spe:10};
|
|
// This is used in gen 1 only, here to avoid code repetition.
|
|
// Only declared if gen 1 to avoid declaring an object we aren't going to need.
|
|
if (this.battle.gen === 1) this.modifiedStats = {atk:0, def:0, spa:0, spd:0, spe:0};
|
|
for (let statName in this.baseStats) {
|
|
let stat = this.template.baseStats[statName];
|
|
stat = Math.floor(Math.floor(2 * stat + this.set.ivs[statName] + Math.floor(this.set.evs[statName] / 4)) * this.level / 100 + 5);
|
|
let nature = this.battle.getNature(this.set.nature);
|
|
if (statName === nature.plus) stat *= 1.1;
|
|
if (statName === nature.minus) stat *= 0.9;
|
|
this.baseStats[statName] = Math.floor(stat);
|
|
}
|
|
|
|
this.maxhp = Math.floor(Math.floor(2 * this.template.baseStats['hp'] + this.set.ivs['hp'] + Math.floor(this.set.evs['hp'] / 4) + 100) * this.level / 100 + 10);
|
|
if (this.template.baseStats['hp'] === 1) this.maxhp = 1; // shedinja
|
|
this.hp = this.hp || this.maxhp;
|
|
|
|
this.isStale = 0;
|
|
this.isStaleCon = 0;
|
|
this.isStaleHP = this.maxhp;
|
|
this.isStalePPTurns = 0;
|
|
|
|
this.baseIvs = this.set.ivs;
|
|
this.baseHpType = this.hpType;
|
|
this.baseHpPower = this.hpPower;
|
|
|
|
this.clearVolatile(true);
|
|
}
|
|
|
|
BattlePokemon.prototype.trapped = false;
|
|
BattlePokemon.prototype.maybeTrapped = false;
|
|
BattlePokemon.prototype.maybeDisabled = false;
|
|
BattlePokemon.prototype.hp = 0;
|
|
BattlePokemon.prototype.maxhp = 100;
|
|
BattlePokemon.prototype.illusion = null;
|
|
BattlePokemon.prototype.fainted = false;
|
|
BattlePokemon.prototype.faintQueued = false;
|
|
BattlePokemon.prototype.lastItem = '';
|
|
BattlePokemon.prototype.ateBerry = false;
|
|
BattlePokemon.prototype.status = '';
|
|
BattlePokemon.prototype.position = 0;
|
|
|
|
BattlePokemon.prototype.lastMove = '';
|
|
BattlePokemon.prototype.moveThisTurn = '';
|
|
|
|
BattlePokemon.prototype.lastDamage = 0;
|
|
BattlePokemon.prototype.lastAttackedBy = null;
|
|
BattlePokemon.prototype.usedItemThisTurn = false;
|
|
BattlePokemon.prototype.newlySwitched = false;
|
|
BattlePokemon.prototype.beingCalledBack = false;
|
|
BattlePokemon.prototype.isActive = false;
|
|
BattlePokemon.prototype.isStarted = false; // has this pokemon's Start events run yet?
|
|
BattlePokemon.prototype.transformed = false;
|
|
BattlePokemon.prototype.duringMove = false;
|
|
BattlePokemon.prototype.hpType = 'Dark';
|
|
BattlePokemon.prototype.hpPower = 60;
|
|
BattlePokemon.prototype.speed = 0;
|
|
|
|
BattlePokemon.prototype.toString = function () {
|
|
let fullname = this.fullname;
|
|
if (this.illusion) fullname = this.illusion.fullname;
|
|
|
|
let positionList = 'abcdef';
|
|
if (this.isActive) return fullname.substr(0, 2) + positionList[this.position] + fullname.substr(2);
|
|
return fullname;
|
|
};
|
|
// "static" function
|
|
BattlePokemon.getDetails = function (side) {
|
|
if (this.illusion) return this.illusion.details + '|' + this.getHealth(side);
|
|
return this.details + '|' + this.getHealth(side);
|
|
};
|
|
BattlePokemon.prototype.update = function (init) {
|
|
this.trapped = this.maybeTrapped = false;
|
|
this.maybeDisabled = false;
|
|
for (let i in this.moveset) {
|
|
if (this.moveset[i]) this.moveset[i].disabled = false;
|
|
}
|
|
if (init) return;
|
|
|
|
// Change formes based on held items (for Transform)
|
|
// Only ever relevant in Generation 4 since Generation 3 didn't have item-based forme changes
|
|
if (this.battle.gen === 4) {
|
|
if (this.template.num === 487) {
|
|
// Giratina formes
|
|
if (this.template.species === 'Giratina' && this.item === 'griseousorb') {
|
|
this.formeChange('Giratina-Origin');
|
|
this.battle.add('-formechange', this, 'Giratina-Origin');
|
|
} else if (this.template.species === 'Giratina-Origin' && this.item !== 'griseousorb') {
|
|
this.formeChange('Giratina');
|
|
this.battle.add('-formechange', this, 'Giratina');
|
|
}
|
|
}
|
|
if (this.template.num === 493) {
|
|
// Arceus formes
|
|
let item = Tools.getItem(this.item);
|
|
let targetForme = (item && item.onPlate ? 'Arceus-' + item.onPlate : 'Arceus');
|
|
if (this.template.species !== targetForme) {
|
|
this.formeChange(targetForme);
|
|
this.battle.add('-formechange', this, targetForme);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.runImmunity('trapped')) this.battle.runEvent('MaybeTrapPokemon', this);
|
|
// Disable the faculty to cancel switches if a foe may have a trapping ability
|
|
for (let i = 0; i < this.battle.sides.length; ++i) {
|
|
let side = this.battle.sides[i];
|
|
if (side === this.side) continue;
|
|
for (let j = 0; j < side.active.length; ++j) {
|
|
let pokemon = side.active[j];
|
|
if (!pokemon || pokemon.fainted) continue;
|
|
let template = (pokemon.illusion || pokemon).template;
|
|
if (!template.abilities) continue;
|
|
for (let k in template.abilities) {
|
|
let ability = template.abilities[k];
|
|
if (ability === pokemon.ability) {
|
|
// This event was already run above so we don't need
|
|
// to run it again.
|
|
continue;
|
|
}
|
|
if ((k === 'H') && template.unreleasedHidden) {
|
|
// unreleased hidden ability
|
|
continue;
|
|
}
|
|
if (this.runImmunity('trapped')) {
|
|
this.battle.singleEvent('FoeMaybeTrapPokemon',
|
|
this.battle.getAbility(ability), {}, this, pokemon);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.battle.runEvent('ModifyPokemon', this);
|
|
|
|
this.speed = this.getStat('spe');
|
|
};
|
|
BattlePokemon.prototype.calculateStat = function (statName, boost, modifier) {
|
|
statName = toId(statName);
|
|
|
|
if (statName === 'hp') return this.maxhp; // please just read .maxhp directly
|
|
|
|
// base stat
|
|
let stat = this.stats[statName];
|
|
|
|
// stat boosts
|
|
// boost = this.boosts[statName];
|
|
let boosts = {};
|
|
boosts[statName] = boost;
|
|
boosts = this.battle.runEvent('ModifyBoost', this, null, null, boosts);
|
|
boost = boosts[statName];
|
|
let boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
|
|
if (boost > 6) boost = 6;
|
|
if (boost < -6) boost = -6;
|
|
if (boost >= 0) {
|
|
stat = Math.floor(stat * boostTable[boost]);
|
|
} else {
|
|
stat = Math.floor(stat / boostTable[-boost]);
|
|
}
|
|
|
|
// stat modifier
|
|
stat = this.battle.modify(stat, (modifier || 1));
|
|
|
|
if (this.battle.getStatCallback) {
|
|
stat = this.battle.getStatCallback(stat, statName, this);
|
|
}
|
|
|
|
return stat;
|
|
};
|
|
BattlePokemon.prototype.getStat = function (statName, unboosted, unmodified) {
|
|
statName = toId(statName);
|
|
|
|
if (statName === 'hp') return this.maxhp; // please just read .maxhp directly
|
|
|
|
// base stat
|
|
let stat = this.stats[statName];
|
|
|
|
// stat boosts
|
|
if (!unboosted) {
|
|
let boosts = this.battle.runEvent('ModifyBoost', this, null, null, Object.clone(this.boosts));
|
|
let boost = boosts[statName];
|
|
let boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
|
|
if (boost > 6) boost = 6;
|
|
if (boost < -6) boost = -6;
|
|
if (boost >= 0) {
|
|
stat = Math.floor(stat * boostTable[boost]);
|
|
} else {
|
|
stat = Math.floor(stat / boostTable[-boost]);
|
|
}
|
|
}
|
|
|
|
// stat modifier effects
|
|
if (!unmodified) {
|
|
let statTable = {atk:'Atk', def:'Def', spa:'SpA', spd:'SpD', spe:'Spe'};
|
|
stat = this.battle.runEvent('Modify' + statTable[statName], this, null, null, stat);
|
|
}
|
|
if (this.battle.getStatCallback) {
|
|
stat = this.battle.getStatCallback(stat, statName, this, unboosted);
|
|
}
|
|
return stat;
|
|
};
|
|
BattlePokemon.prototype.getWeight = function () {
|
|
let weight = this.template.weightkg;
|
|
weight = this.battle.runEvent('ModifyWeight', this, null, null, weight);
|
|
if (weight < 0.1) weight = 0.1;
|
|
return weight;
|
|
};
|
|
BattlePokemon.prototype.getMoveData = function (move) {
|
|
move = this.battle.getMove(move);
|
|
for (let i = 0; i < this.moveset.length; i++) {
|
|
let moveData = this.moveset[i];
|
|
if (moveData.id === move.id) {
|
|
return moveData;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
BattlePokemon.prototype.getMoveTargets = function (move, target) {
|
|
let targets = [];
|
|
switch (move.target) {
|
|
case 'all':
|
|
case 'foeSide':
|
|
case 'allySide':
|
|
case 'allyTeam':
|
|
if (!move.target.startsWith('foe')) {
|
|
for (let i = 0; i < this.side.active.length; i++) {
|
|
if (this.side.active[i] && !this.side.active[i].fainted) {
|
|
targets.push(this.side.active[i]);
|
|
}
|
|
}
|
|
}
|
|
if (!move.target.startsWith('ally')) {
|
|
for (let i = 0; i < this.side.foe.active.length; i++) {
|
|
if (this.side.foe.active[i] && !this.side.foe.active[i].fainted) {
|
|
targets.push(this.side.foe.active[i]);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'allAdjacent':
|
|
case 'allAdjacentFoes':
|
|
if (move.target === 'allAdjacent') {
|
|
for (let i = 0; i < this.side.active.length; i++) {
|
|
if (this.side.active[i] && this.battle.isAdjacent(this, this.side.active[i])) {
|
|
targets.push(this.side.active[i]);
|
|
}
|
|
}
|
|
}
|
|
for (let i = 0; i < this.side.foe.active.length; i++) {
|
|
if (this.side.foe.active[i] && this.battle.isAdjacent(this, this.side.foe.active[i])) {
|
|
targets.push(this.side.foe.active[i]);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
if (!target || (target.fainted && target.side !== this.side)) {
|
|
// If a targeted foe faints, the move is retargeted
|
|
target = this.battle.resolveTarget(this, move);
|
|
}
|
|
if (target.side.active.length > 1) {
|
|
target = this.battle.runEvent('RedirectTarget', this, this, move, target);
|
|
}
|
|
targets = [target];
|
|
|
|
// Resolve apparent targets for Pressure.
|
|
if (move.pressureTarget) {
|
|
// At the moment, this is the only supported target.
|
|
if (move.pressureTarget === 'foeSide') {
|
|
for (let i = 0; i < this.side.foe.active.length; i++) {
|
|
if (this.side.foe.active[i] && !this.side.foe.active[i].fainted) {
|
|
targets.push(this.side.foe.active[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return targets;
|
|
};
|
|
BattlePokemon.prototype.ignoringAbility = function () {
|
|
return !!((this.battle.gen >= 5 && !this.isActive) || this.volatiles['gastroacid']);
|
|
};
|
|
BattlePokemon.prototype.ignoringItem = function () {
|
|
return !!((this.battle.gen >= 5 && !this.isActive) || this.hasAbility('klutz') || this.volatiles['embargo'] || this.battle.pseudoWeather['magicroom']);
|
|
};
|
|
BattlePokemon.prototype.deductPP = function (move, amount, source) {
|
|
move = this.battle.getMove(move);
|
|
let ppData = this.getMoveData(move);
|
|
if (!ppData) return false;
|
|
ppData.used = true;
|
|
if (!ppData.pp) return false;
|
|
|
|
ppData.pp -= amount || 1;
|
|
if (ppData.pp <= 0) {
|
|
ppData.pp = 0;
|
|
}
|
|
if (ppData.virtual) {
|
|
let foeActive = this.side.foe.active;
|
|
for (let i = 0; i < foeActive.length; i++) {
|
|
if (foeActive[i].isStale >= 2) {
|
|
if (move.selfSwitch) this.isStalePPTurns++;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
this.isStalePPTurns = 0;
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.moveUsed = function (move) {
|
|
this.lastMove = this.battle.getMove(move).id;
|
|
this.moveThisTurn = this.lastMove;
|
|
};
|
|
BattlePokemon.prototype.gotAttacked = function (move, damage, source) {
|
|
if (!damage) damage = 0;
|
|
move = this.battle.getMove(move);
|
|
this.lastAttackedBy = {
|
|
pokemon: source,
|
|
damage: damage,
|
|
move: move.id,
|
|
thisTurn: true
|
|
};
|
|
};
|
|
BattlePokemon.prototype.getLockedMove = function () {
|
|
let lockedMove = this.battle.runEvent('LockMove', this);
|
|
if (lockedMove === true) lockedMove = false;
|
|
return lockedMove;
|
|
};
|
|
BattlePokemon.prototype.getMoves = function (lockedMove, restrictData) {
|
|
if (lockedMove) {
|
|
lockedMove = toId(lockedMove);
|
|
this.trapped = true;
|
|
}
|
|
if (lockedMove === 'recharge') {
|
|
return [{
|
|
move: 'Recharge',
|
|
id: 'recharge'
|
|
}];
|
|
}
|
|
let moves = [];
|
|
let hasValidMove = false;
|
|
for (let i = 0; i < this.moveset.length; i++) {
|
|
let move = this.moveset[i];
|
|
if (lockedMove) {
|
|
if (lockedMove === move.id) {
|
|
return [{
|
|
move: move.move,
|
|
id: move.id
|
|
}];
|
|
}
|
|
continue;
|
|
}
|
|
if (this.disabledMoves[move.id] && (!restrictData || !this.disabledMoves[move.id].isHidden) || move.pp <= 0 && (this.battle.gen !== 1 || !this.volatiles['partialtrappinglock'])) {
|
|
move.disabled = !restrictData && this.disabledMoves[move.id] && this.disabledMoves[move.id].isHidden ? 'hidden' : true;
|
|
} else if (!move.disabled || move.disabled === 'hidden' && restrictData) {
|
|
hasValidMove = true;
|
|
}
|
|
let moveName = move.move;
|
|
if (move.id === 'hiddenpower') {
|
|
moveName = 'Hidden Power ' + this.hpType;
|
|
if (this.battle.gen < 6) moveName += ' ' + this.hpPower;
|
|
}
|
|
moves.push({
|
|
move: moveName,
|
|
id: move.id,
|
|
pp: move.pp,
|
|
maxpp: move.maxpp,
|
|
target: move.target,
|
|
disabled: move.disabled
|
|
});
|
|
}
|
|
if (lockedMove) {
|
|
return [{
|
|
move: this.battle.getMove(lockedMove).name,
|
|
id: lockedMove
|
|
}];
|
|
}
|
|
if (hasValidMove) return moves;
|
|
|
|
return [];
|
|
};
|
|
BattlePokemon.prototype.getRequestData = function () {
|
|
let lockedMove = this.getLockedMove();
|
|
|
|
// Information should be restricted for the last active Pokémon
|
|
let isLastActive = this.isLastActive();
|
|
let moves = this.getMoves(lockedMove, isLastActive);
|
|
let data = {moves: moves.length ? moves : [{move: 'Struggle', id: 'struggle'}]};
|
|
|
|
if (isLastActive) {
|
|
if (this.maybeDisabled) {
|
|
data.maybeDisabled = true;
|
|
}
|
|
if (this.trapped === true) {
|
|
data.trapped = true;
|
|
} else if (this.maybeTrapped) {
|
|
data.maybeTrapped = true;
|
|
}
|
|
} else {
|
|
if (this.trapped) data.trapped = true;
|
|
}
|
|
|
|
return data;
|
|
};
|
|
BattlePokemon.prototype.isLastActive = function () {
|
|
if (!this.isActive) return false;
|
|
|
|
let allyActive = this.side.active;
|
|
for (let i = this.position + 1; i < allyActive.length; i++) {
|
|
if (allyActive[i] && !allyActive.fainted) return false;
|
|
}
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.positiveBoosts = function () {
|
|
let boosts = 0;
|
|
for (let i in this.boosts) {
|
|
if (this.boosts[i] > 0) boosts += this.boosts[i];
|
|
}
|
|
return boosts;
|
|
};
|
|
BattlePokemon.prototype.boostBy = function (boost) {
|
|
let changed = false;
|
|
for (let i in boost) {
|
|
let delta = boost[i];
|
|
this.boosts[i] += delta;
|
|
if (this.boosts[i] > 6) {
|
|
delta -= this.boosts[i] - 6;
|
|
this.boosts[i] = 6;
|
|
}
|
|
if (this.boosts[i] < -6) {
|
|
delta -= this.boosts[i] - (-6);
|
|
this.boosts[i] = -6;
|
|
}
|
|
if (delta) changed = true;
|
|
}
|
|
this.update();
|
|
return changed;
|
|
};
|
|
BattlePokemon.prototype.clearBoosts = function () {
|
|
for (let i in this.boosts) {
|
|
this.boosts[i] = 0;
|
|
}
|
|
this.update();
|
|
};
|
|
BattlePokemon.prototype.setBoost = function (boost) {
|
|
for (let i in boost) {
|
|
this.boosts[i] = boost[i];
|
|
}
|
|
this.update();
|
|
};
|
|
BattlePokemon.prototype.copyVolatileFrom = function (pokemon) {
|
|
this.clearVolatile();
|
|
this.boosts = pokemon.boosts;
|
|
for (let i in pokemon.volatiles) {
|
|
if (this.battle.getEffect(i).noCopy) continue;
|
|
// shallow clones
|
|
this.volatiles[i] = Object.clone(pokemon.volatiles[i]);
|
|
if (this.volatiles[i].linkedPokemon) {
|
|
delete pokemon.volatiles[i].linkedPokemon;
|
|
delete pokemon.volatiles[i].linkedStatus;
|
|
this.volatiles[i].linkedPokemon.volatiles[this.volatiles[i].linkedStatus].linkedPokemon = this;
|
|
}
|
|
}
|
|
pokemon.clearVolatile();
|
|
this.update();
|
|
for (let i in this.volatiles) {
|
|
this.battle.singleEvent('Copy', this.getVolatile(i), this.volatiles[i], this);
|
|
}
|
|
};
|
|
BattlePokemon.prototype.transformInto = function (pokemon, user, effect) {
|
|
let template = pokemon.template;
|
|
if (pokemon.fainted || pokemon.illusion || (pokemon.volatiles['substitute'] && this.battle.gen >= 5)) {
|
|
return false;
|
|
}
|
|
if (!template.abilities || (pokemon && pokemon.transformed && this.battle.gen >= 2) || (user && user.transformed && this.battle.gen >= 5)) {
|
|
return false;
|
|
}
|
|
if (!this.formeChange(template, true)) {
|
|
return false;
|
|
}
|
|
this.transformed = true;
|
|
this.typesData = [];
|
|
for (let i = 0, l = pokemon.typesData.length; i < l; i++) {
|
|
this.typesData.push({
|
|
type: pokemon.typesData[i].type,
|
|
suppressed: false,
|
|
isAdded: pokemon.typesData[i].isAdded
|
|
});
|
|
}
|
|
for (let statName in this.stats) {
|
|
this.stats[statName] = pokemon.stats[statName];
|
|
}
|
|
this.moveset = [];
|
|
this.moves = [];
|
|
this.set.ivs = (this.battle.gen >= 5 ? this.set.ivs : pokemon.set.ivs);
|
|
this.hpType = (this.battle.gen >= 5 ? this.hpType : pokemon.hpType);
|
|
this.hpPower = (this.battle.gen >= 5 ? this.hpPower : pokemon.hpPower);
|
|
for (let i = 0; i < pokemon.moveset.length; i++) {
|
|
let moveData = pokemon.moveset[i];
|
|
let moveName = moveData.move;
|
|
if (moveData.id === 'hiddenpower') {
|
|
moveName = 'Hidden Power ' + this.hpType;
|
|
}
|
|
this.moveset.push({
|
|
move: moveName,
|
|
id: moveData.id,
|
|
pp: moveData.maxpp === 1 ? 1 : 5,
|
|
maxpp: this.battle.gen >= 5 ? (moveData.maxpp === 1 ? 1 : 5) : moveData.maxpp,
|
|
target: moveData.target,
|
|
disabled: false,
|
|
used: false,
|
|
virtual: true
|
|
});
|
|
this.moves.push(toId(moveName));
|
|
}
|
|
for (let j in pokemon.boosts) {
|
|
this.boosts[j] = pokemon.boosts[j];
|
|
}
|
|
if (effect) {
|
|
this.battle.add('-transform', this, pokemon, '[from] ' + effect.fullname);
|
|
} else {
|
|
this.battle.add('-transform', this, pokemon);
|
|
}
|
|
this.setAbility(pokemon.ability);
|
|
this.update();
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.formeChange = function (template, dontRecalculateStats) {
|
|
template = this.battle.getTemplate(template);
|
|
|
|
if (!template.abilities) return false;
|
|
this.illusion = null;
|
|
this.template = template;
|
|
this.types = template.types;
|
|
this.typesData = [];
|
|
this.types = template.types;
|
|
for (let i = 0, l = this.types.length; i < l; i++) {
|
|
this.typesData.push({
|
|
type: this.types[i],
|
|
suppressed: false,
|
|
isAdded: false
|
|
});
|
|
}
|
|
if (!dontRecalculateStats) {
|
|
for (let statName in this.stats) {
|
|
let stat = this.template.baseStats[statName];
|
|
stat = Math.floor(Math.floor(2 * stat + this.set.ivs[statName] + Math.floor(this.set.evs[statName] / 4)) * this.level / 100 + 5);
|
|
|
|
// nature
|
|
let nature = this.battle.getNature(this.set.nature);
|
|
if (statName === nature.plus) stat *= 1.1;
|
|
if (statName === nature.minus) stat *= 0.9;
|
|
this.baseStats[statName] = this.stats[statName] = Math.floor(stat);
|
|
// If gen 1, we reset modified stats.
|
|
if (this.battle.gen === 1) {
|
|
this.modifiedStats[statName] = Math.floor(stat);
|
|
// ...and here is where the gen 1 games re-apply burn and para drops.
|
|
if (this.status === 'par' && statName === 'spe') this.modifyStat('spe', 0.25);
|
|
if (this.status === 'brn' && statName === 'atk') this.modifyStat('atk', 0.5);
|
|
}
|
|
}
|
|
this.speed = this.stats.spe;
|
|
}
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.clearVolatile = function (init) {
|
|
this.boosts = {
|
|
atk: 0,
|
|
def: 0,
|
|
spa: 0,
|
|
spd: 0,
|
|
spe: 0,
|
|
accuracy: 0,
|
|
evasion: 0
|
|
};
|
|
|
|
if (this.battle.gen === 1 && this.baseMoves.indexOf('mimic') >= 0 && !this.transformed) {
|
|
let moveslot = this.baseMoves.indexOf('mimic');
|
|
let mimicPP = this.moveset[moveslot] ? this.moveset[moveslot].pp : 16;
|
|
this.moveset = this.baseMoveset.slice();
|
|
this.moveset[moveslot].pp = mimicPP;
|
|
} else {
|
|
this.moveset = this.baseMoveset.slice();
|
|
}
|
|
this.moves = this.moveset.map(function (move) {
|
|
return toId(move.move);
|
|
});
|
|
|
|
this.transformed = false;
|
|
this.ability = this.baseAbility;
|
|
this.set.ivs = this.baseIvs;
|
|
this.hpType = this.baseHpType;
|
|
this.hpPower = this.baseHpPower;
|
|
for (let i in this.volatiles) {
|
|
if (this.volatiles[i].linkedStatus) {
|
|
this.volatiles[i].linkedPokemon.removeVolatile(this.volatiles[i].linkedStatus);
|
|
}
|
|
}
|
|
this.volatiles = {};
|
|
this.switchFlag = false;
|
|
|
|
this.lastMove = '';
|
|
this.moveThisTurn = '';
|
|
|
|
this.lastDamage = 0;
|
|
this.lastAttackedBy = null;
|
|
this.newlySwitched = true;
|
|
this.beingCalledBack = false;
|
|
|
|
this.formeChange(this.baseTemplate);
|
|
|
|
this.update(init);
|
|
};
|
|
BattlePokemon.prototype.hasType = function (type) {
|
|
if (!type) return false;
|
|
if (Array.isArray(type)) {
|
|
for (let i = 0; i < type.length; i++) {
|
|
if (this.hasType(type[i])) return true;
|
|
}
|
|
} else {
|
|
if (this.getTypes().indexOf(type) >= 0) return true;
|
|
}
|
|
return false;
|
|
};
|
|
// returns the amount of damage actually dealt
|
|
BattlePokemon.prototype.faint = function (source, effect) {
|
|
// This function only puts the pokemon in the faint queue;
|
|
// actually setting of this.fainted comes later when the
|
|
// faint queue is resolved.
|
|
if (this.fainted || this.faintQueued) return 0;
|
|
let d = this.hp;
|
|
this.hp = 0;
|
|
this.switchFlag = false;
|
|
this.faintQueued = true;
|
|
this.battle.faintQueue.push({
|
|
target: this,
|
|
source: source,
|
|
effect: effect
|
|
});
|
|
return d;
|
|
};
|
|
BattlePokemon.prototype.damage = function (d, source, effect) {
|
|
if (!this.hp) return 0;
|
|
if (d < 1 && d > 0) d = 1;
|
|
d = Math.floor(d);
|
|
if (isNaN(d)) return 0;
|
|
if (d <= 0) return 0;
|
|
this.hp -= d;
|
|
if (this.hp <= 0) {
|
|
d += this.hp;
|
|
this.faint(source, effect);
|
|
}
|
|
return d;
|
|
};
|
|
BattlePokemon.prototype.tryTrap = function (isHidden) {
|
|
if (this.runImmunity('trapped')) {
|
|
if (this.trapped && isHidden) return true;
|
|
this.trapped = isHidden ? 'hidden' : true;
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.hasMove = function (moveid) {
|
|
moveid = toId(moveid);
|
|
if (moveid.substr(0, 11) === 'hiddenpower') moveid = 'hiddenpower';
|
|
for (let i = 0; i < this.moveset.length; i++) {
|
|
if (moveid === this.battle.getMove(this.moveset[i].move).id) {
|
|
return moveid;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.disableMove = function (moveid, isHidden, sourceEffect) {
|
|
if (!sourceEffect && this.battle.event) {
|
|
sourceEffect = this.battle.effect;
|
|
}
|
|
moveid = toId(moveid);
|
|
if (moveid.substr(0, 11) === 'hiddenpower') moveid = 'hiddenpower';
|
|
|
|
if (this.disabledMoves[moveid] && !this.disabledMoves[moveid].isHidden) return;
|
|
this.disabledMoves[moveid] = {
|
|
isHidden: !!isHidden,
|
|
sourceEffect: sourceEffect
|
|
};
|
|
};
|
|
// returns the amount of damage actually healed
|
|
BattlePokemon.prototype.heal = function (d) {
|
|
if (!this.hp) return false;
|
|
d = Math.floor(d);
|
|
if (isNaN(d)) return false;
|
|
if (d <= 0) return false;
|
|
if (this.hp >= this.maxhp) return false;
|
|
this.hp += d;
|
|
if (this.hp > this.maxhp) {
|
|
d -= this.hp - this.maxhp;
|
|
this.hp = this.maxhp;
|
|
}
|
|
return d;
|
|
};
|
|
// sets HP, returns delta
|
|
BattlePokemon.prototype.sethp = function (d) {
|
|
if (!this.hp) return 0;
|
|
d = Math.floor(d);
|
|
if (isNaN(d)) return;
|
|
if (d < 1) d = 1;
|
|
d = d - this.hp;
|
|
this.hp += d;
|
|
if (this.hp > this.maxhp) {
|
|
d -= this.hp - this.maxhp;
|
|
this.hp = this.maxhp;
|
|
}
|
|
return d;
|
|
};
|
|
BattlePokemon.prototype.trySetStatus = function (status, source, sourceEffect) {
|
|
if (!this.hp) return false;
|
|
if (this.status) return false;
|
|
return this.setStatus(status, source, sourceEffect);
|
|
};
|
|
BattlePokemon.prototype.cureStatus = function () {
|
|
if (!this.hp) return false;
|
|
// unlike clearStatus, gives cure message
|
|
if (this.status) {
|
|
this.battle.add('-curestatus', this, this.status);
|
|
this.setStatus('');
|
|
}
|
|
};
|
|
BattlePokemon.prototype.setStatus = function (status, source, sourceEffect, ignoreImmunities) {
|
|
if (!this.hp) return false;
|
|
status = this.battle.getEffect(status);
|
|
if (this.battle.event) {
|
|
if (!source) source = this.battle.event.source;
|
|
if (!sourceEffect) sourceEffect = this.battle.effect;
|
|
}
|
|
|
|
if (!ignoreImmunities && status.id) {
|
|
// the game currently never ignores immunities
|
|
if (!this.runImmunity(status.id === 'tox' ? 'psn' : status.id)) {
|
|
this.battle.debug('immune to status');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (this.status === status.id) return false;
|
|
let prevStatus = this.status;
|
|
let prevStatusData = this.statusData;
|
|
if (status.id && !this.battle.runEvent('SetStatus', this, source, sourceEffect, status)) {
|
|
this.battle.debug('set status [' + status.id + '] interrupted');
|
|
return false;
|
|
}
|
|
|
|
this.status = status.id;
|
|
this.statusData = {id: status.id, target: this};
|
|
if (source) this.statusData.source = source;
|
|
if (status.duration) {
|
|
this.statusData.duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.statusData.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
|
|
if (status.id && !this.battle.singleEvent('Start', status, this.statusData, this, source, sourceEffect)) {
|
|
this.battle.debug('status start [' + status.id + '] interrupted');
|
|
// cancel the setstatus
|
|
this.status = prevStatus;
|
|
this.statusData = prevStatusData;
|
|
return false;
|
|
}
|
|
this.update();
|
|
if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.clearStatus = function () {
|
|
// unlike cureStatus, does not give cure message
|
|
return this.setStatus('');
|
|
};
|
|
BattlePokemon.prototype.getStatus = function () {
|
|
return this.battle.getEffect(this.status);
|
|
};
|
|
BattlePokemon.prototype.eatItem = function (item, source, sourceEffect) {
|
|
if (!this.hp || !this.isActive) return false;
|
|
if (!this.item) return false;
|
|
|
|
let id = toId(item);
|
|
if (id && this.item !== id) return false;
|
|
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
|
|
item = this.getItem();
|
|
if (this.battle.runEvent('UseItem', this, null, null, item) && this.battle.runEvent('EatItem', this, null, null, item)) {
|
|
this.battle.add('-enditem', this, item, '[eat]');
|
|
|
|
this.battle.singleEvent('Eat', item, this.itemData, this, source, sourceEffect);
|
|
|
|
this.lastItem = this.item;
|
|
this.item = '';
|
|
this.itemData = {id: '', target: this};
|
|
this.usedItemThisTurn = true;
|
|
this.ateBerry = true;
|
|
this.battle.runEvent('AfterUseItem', this, null, null, item);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.useItem = function (item, source, sourceEffect) {
|
|
if (!this.isActive) return false;
|
|
if (!this.item) return false;
|
|
|
|
let id = toId(item);
|
|
if (id && this.item !== id) return false;
|
|
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
|
|
item = this.getItem();
|
|
if (this.battle.runEvent('UseItem', this, null, null, item)) {
|
|
switch (item.id) {
|
|
case 'redcard':
|
|
this.battle.add('-enditem', this, item, '[of] ' + source);
|
|
break;
|
|
default:
|
|
if (!item.isGem) {
|
|
this.battle.add('-enditem', this, item);
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.battle.singleEvent('Use', item, this.itemData, this, source, sourceEffect);
|
|
|
|
this.lastItem = this.item;
|
|
this.item = '';
|
|
this.itemData = {id: '', target: this};
|
|
this.usedItemThisTurn = true;
|
|
this.battle.runEvent('AfterUseItem', this, null, null, item);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.takeItem = function (source) {
|
|
if (!this.isActive) return false;
|
|
if (!this.item) return false;
|
|
if (!source) source = this;
|
|
if (this.battle.gen === 4) {
|
|
if (toId(this.ability) === 'multitype') return false;
|
|
if (source && toId(source.ability) === 'multitype') return false;
|
|
}
|
|
let item = this.getItem();
|
|
if (this.battle.runEvent('TakeItem', this, source, null, item)) {
|
|
this.item = '';
|
|
this.itemData = {id: '', target: this};
|
|
return item;
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.setItem = function (item, source, effect) {
|
|
if (!this.hp || !this.isActive) return false;
|
|
item = this.battle.getItem(item);
|
|
if (item.id === 'leppaberry') {
|
|
this.isStale = 2;
|
|
this.isStaleSource = 'getleppa';
|
|
}
|
|
this.lastItem = this.item;
|
|
this.item = item.id;
|
|
this.itemData = {id: item.id, target: this};
|
|
if (item.id) {
|
|
this.battle.singleEvent('Start', item, this.itemData, this, source, effect);
|
|
}
|
|
if (this.lastItem) this.usedItemThisTurn = true;
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.getItem = function () {
|
|
return this.battle.getItem(this.item);
|
|
};
|
|
BattlePokemon.prototype.hasItem = function (item) {
|
|
if (this.ignoringItem()) return false;
|
|
let ownItem = this.item;
|
|
if (!Array.isArray(item)) {
|
|
return ownItem === toId(item);
|
|
}
|
|
return (item.map(toId).indexOf(ownItem) >= 0);
|
|
};
|
|
BattlePokemon.prototype.clearItem = function () {
|
|
return this.setItem('');
|
|
};
|
|
BattlePokemon.prototype.setAbility = function (ability, source, effect, noForce) {
|
|
if (!this.hp) return false;
|
|
ability = this.battle.getAbility(ability);
|
|
let oldAbility = this.ability;
|
|
if (noForce && oldAbility === ability.id) {
|
|
return false;
|
|
}
|
|
if (ability.id in {illusion:1, multitype:1, stancechange:1}) return false;
|
|
if (oldAbility in {multitype:1, stancechange:1}) return false;
|
|
this.battle.singleEvent('End', this.battle.getAbility(oldAbility), this.abilityData, this, source, effect);
|
|
this.ability = ability.id;
|
|
this.abilityData = {id: ability.id, target: this};
|
|
if (ability.id && this.battle.gen > 3) {
|
|
this.battle.singleEvent('Start', ability, this.abilityData, this, source, effect);
|
|
}
|
|
return oldAbility;
|
|
};
|
|
BattlePokemon.prototype.getAbility = function () {
|
|
return this.battle.getAbility(this.ability);
|
|
};
|
|
BattlePokemon.prototype.hasAbility = function (ability) {
|
|
if (this.ignoringAbility()) return false;
|
|
let ownAbility = this.ability;
|
|
if (!Array.isArray(ability)) {
|
|
return ownAbility === toId(ability);
|
|
}
|
|
return (ability.map(toId).indexOf(ownAbility) >= 0);
|
|
};
|
|
BattlePokemon.prototype.clearAbility = function () {
|
|
return this.setAbility('');
|
|
};
|
|
BattlePokemon.prototype.getNature = function () {
|
|
return this.battle.getNature(this.set.nature);
|
|
};
|
|
BattlePokemon.prototype.addVolatile = function (status, source, sourceEffect, linkedStatus) {
|
|
let result;
|
|
status = this.battle.getEffect(status);
|
|
if (!this.hp && !status.affectsFainted) return false;
|
|
if (this.battle.event) {
|
|
if (!source) source = this.battle.event.source;
|
|
if (!sourceEffect) sourceEffect = this.battle.effect;
|
|
}
|
|
|
|
if (this.volatiles[status.id]) {
|
|
if (!status.onRestart) return false;
|
|
return this.battle.singleEvent('Restart', status, this.volatiles[status.id], this, source, sourceEffect);
|
|
}
|
|
if (!this.runImmunity(status.id)) return false;
|
|
result = this.battle.runEvent('TryAddVolatile', this, source, sourceEffect, status);
|
|
if (!result) {
|
|
this.battle.debug('add volatile [' + status.id + '] interrupted');
|
|
return result;
|
|
}
|
|
this.volatiles[status.id] = {id: status.id};
|
|
this.volatiles[status.id].target = this;
|
|
if (source) {
|
|
this.volatiles[status.id].source = source;
|
|
this.volatiles[status.id].sourcePosition = source.position;
|
|
}
|
|
if (sourceEffect) {
|
|
this.volatiles[status.id].sourceEffect = sourceEffect;
|
|
}
|
|
if (status.duration) {
|
|
this.volatiles[status.id].duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.volatiles[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
result = this.battle.singleEvent('Start', status, this.volatiles[status.id], this, source, sourceEffect);
|
|
if (!result) {
|
|
// cancel
|
|
delete this.volatiles[status.id];
|
|
return result;
|
|
}
|
|
if (linkedStatus && source && !source.volatiles[linkedStatus]) {
|
|
source.addVolatile(linkedStatus, this, sourceEffect, status);
|
|
source.volatiles[linkedStatus].linkedPokemon = this;
|
|
source.volatiles[linkedStatus].linkedStatus = status;
|
|
this.volatiles[status].linkedPokemon = source;
|
|
this.volatiles[status].linkedStatus = linkedStatus;
|
|
}
|
|
this.update();
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.getVolatile = function (status) {
|
|
status = this.battle.getEffect(status);
|
|
if (!this.volatiles[status.id]) return null;
|
|
return status;
|
|
};
|
|
BattlePokemon.prototype.removeVolatile = function (status) {
|
|
if (!this.hp) return false;
|
|
status = this.battle.getEffect(status);
|
|
if (!this.volatiles[status.id]) return false;
|
|
this.battle.singleEvent('End', status, this.volatiles[status.id], this);
|
|
let linkedPokemon = this.volatiles[status.id].linkedPokemon;
|
|
let linkedStatus = this.volatiles[status.id].linkedStatus;
|
|
delete this.volatiles[status.id];
|
|
if (linkedPokemon && linkedPokemon.volatiles[linkedStatus]) {
|
|
linkedPokemon.removeVolatile(linkedStatus);
|
|
}
|
|
this.update();
|
|
return true;
|
|
};
|
|
// "static" function
|
|
BattlePokemon.getHealth = function (side) {
|
|
if (!this.hp) return '0 fnt';
|
|
let hpstring;
|
|
if ((side === true) || (this.side === side) || this.battle.getFormat().debug || this.battle.reportExactHP) {
|
|
hpstring = '' + this.hp + '/' + this.maxhp;
|
|
} else {
|
|
let ratio = this.hp / this.maxhp;
|
|
if (this.battle.reportPercentages) {
|
|
// HP Percentage Mod mechanics
|
|
let percentage = Math.ceil(ratio * 100);
|
|
if ((percentage === 100) && (ratio < 1.0)) {
|
|
percentage = 99;
|
|
}
|
|
hpstring = '' + percentage + '/100';
|
|
} else {
|
|
// In-game accurate pixel health mechanics
|
|
let pixels = Math.floor(ratio * 48) || 1;
|
|
hpstring = '' + pixels + '/48';
|
|
if ((pixels === 9) && (ratio > 0.2)) {
|
|
hpstring += 'y'; // force yellow HP bar
|
|
} else if ((pixels === 24) && (ratio > 0.5)) {
|
|
hpstring += 'g'; // force green HP bar
|
|
}
|
|
}
|
|
}
|
|
if (this.status) hpstring += ' ' + this.status;
|
|
return hpstring;
|
|
};
|
|
BattlePokemon.prototype.setType = function (newType, enforce) {
|
|
// Arceus first type cannot be normally changed
|
|
if (!enforce && this.template.num === 493) return false;
|
|
|
|
this.typesData = [{
|
|
type: newType,
|
|
suppressed: false,
|
|
isAdded: false
|
|
}];
|
|
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.addType = function (newType) {
|
|
// removes any types added previously and adds another one
|
|
|
|
this.typesData = this.typesData.filter(function (typeData) {
|
|
return !typeData.isAdded;
|
|
}).concat([{
|
|
type: newType,
|
|
suppressed: false,
|
|
isAdded: true
|
|
}]);
|
|
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.getTypes = function (getAll) {
|
|
let types = [];
|
|
for (let i = 0, l = this.typesData.length; i < l; i++) {
|
|
if (getAll || !this.typesData[i].suppressed) {
|
|
types.push(this.typesData[i].type);
|
|
}
|
|
}
|
|
if (types.length) return types;
|
|
if (this.battle.gen >= 5) return ['Normal'];
|
|
return ['???'];
|
|
};
|
|
BattlePokemon.prototype.isGrounded = function () {
|
|
if (!this.hasType('Flying') && this.battle.runEvent('Immunity', this, null, null, 'Ground')) return true;
|
|
return !!(this.hasItem('ironball') || this.volatiles['ingrain'] || this.volatiles['smackdown'] || this.battle.getPseudoWeather('gravity'));
|
|
};
|
|
BattlePokemon.prototype.isSemiInvulnerable = function () {
|
|
if (this.volatiles['fly'] || this.volatiles['bounce'] || this.volatiles['skydrop'] || this.volatiles['dive'] || this.volatiles['dig'] || this.volatiles['phantomforce'] || this.volatiles['shadowforce']) {
|
|
return true;
|
|
}
|
|
for (let i = 0; i < this.side.foe.active.length; i++) {
|
|
if (this.side.foe.active[i].volatiles['skydrop'] && this.side.foe.active[i].volatiles['skydrop'].source === this) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
BattlePokemon.prototype.runEffectiveness = function (move) {
|
|
let totalTypeMod = 0;
|
|
let types = this.getTypes();
|
|
for (let i = 0; i < types.length; i++) {
|
|
let typeMod = this.battle.getEffectiveness(move, types[i]);
|
|
typeMod = this.battle.singleEvent('Effectiveness', move, null, types[i], move, null, typeMod);
|
|
totalTypeMod += this.battle.runEvent('Effectiveness', this, types[i], move, typeMod);
|
|
}
|
|
return totalTypeMod;
|
|
};
|
|
BattlePokemon.prototype.runImmunity = function (type, message) {
|
|
if (this.fainted) {
|
|
return false;
|
|
}
|
|
if (!type || type === '???') {
|
|
return true;
|
|
}
|
|
if (!this.battle.runEvent('NegateImmunity', this, type)) return true;
|
|
if (!this.battle.getImmunity(type, this)) {
|
|
this.battle.debug('natural immunity');
|
|
if (message) {
|
|
this.battle.add('-immune', this, '[msg]');
|
|
}
|
|
return false;
|
|
}
|
|
let immunity = this.battle.runEvent('Immunity', this, null, null, type);
|
|
if (!immunity) {
|
|
this.battle.debug('artificial immunity');
|
|
if (message && immunity !== null) {
|
|
this.battle.add('-immune', this, '[msg]');
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
BattlePokemon.prototype.destroy = function () {
|
|
// deallocate ourself
|
|
// get rid of some possibly-circular references
|
|
this.battle = null;
|
|
this.side = null;
|
|
};
|
|
return BattlePokemon;
|
|
})();
|
|
|
|
BattleSide = (function () {
|
|
function BattleSide(name, battle, n, team) {
|
|
let sideScripts = battle.data.Scripts.side;
|
|
if (sideScripts) Object.merge(this, sideScripts);
|
|
|
|
this.getChoice = (this.getChoice || BattleSide.getChoice).bind(this);
|
|
|
|
this.battle = battle;
|
|
this.n = n;
|
|
this.name = name;
|
|
this.pokemon = [];
|
|
this.active = [null];
|
|
this.sideConditions = {};
|
|
|
|
this.id = n ? 'p2' : 'p1';
|
|
|
|
switch (this.battle.gameType) {
|
|
case 'doubles':
|
|
this.active = [null, null];
|
|
break;
|
|
case 'triples': case 'rotation':
|
|
this.active = [null, null, null];
|
|
break;
|
|
}
|
|
|
|
this.team = this.battle.getTeam(this, team);
|
|
for (let i = 0; i < this.team.length && i < 6; i++) {
|
|
//console.log("NEW POKEMON: " + (this.team[i] ? this.team[i].name : '[unidentified]'));
|
|
this.pokemon.push(new BattlePokemon(this.team[i], this));
|
|
}
|
|
this.pokemonLeft = this.pokemon.length;
|
|
for (let i = 0; i < this.pokemon.length; i++) {
|
|
this.pokemon[i].position = i;
|
|
}
|
|
}
|
|
|
|
BattleSide.getChoice = function (side) {
|
|
if (side !== this && side !== true) return '';
|
|
return this.choice;
|
|
};
|
|
|
|
BattleSide.prototype.isActive = false;
|
|
BattleSide.prototype.pokemonLeft = 0;
|
|
BattleSide.prototype.faintedLastTurn = false;
|
|
BattleSide.prototype.faintedThisTurn = false;
|
|
BattleSide.prototype.decision = null;
|
|
BattleSide.prototype.foe = null;
|
|
|
|
BattleSide.prototype.toString = function () {
|
|
return this.id + ': ' + this.name;
|
|
};
|
|
BattleSide.prototype.getData = function () {
|
|
let data = {
|
|
name: this.name,
|
|
id: this.id,
|
|
pokemon: []
|
|
};
|
|
for (let i = 0; i < this.pokemon.length; i++) {
|
|
let pokemon = this.pokemon[i];
|
|
data.pokemon.push({
|
|
ident: pokemon.fullname,
|
|
details: pokemon.details,
|
|
condition: pokemon.getHealth(pokemon.side),
|
|
active: (pokemon.position < pokemon.side.active.length),
|
|
stats: {
|
|
atk: pokemon.baseStats['atk'],
|
|
def: pokemon.baseStats['def'],
|
|
spa: pokemon.baseStats['spa'],
|
|
spd: pokemon.baseStats['spd'],
|
|
spe: pokemon.baseStats['spe']
|
|
},
|
|
moves: pokemon.moves.map(function (move) {
|
|
if (move === 'hiddenpower') {
|
|
return move + toId(pokemon.hpType) + (pokemon.hpPower === 70 ? '' : pokemon.hpPower);
|
|
}
|
|
return move;
|
|
}),
|
|
baseAbility: pokemon.baseAbility,
|
|
item: pokemon.item,
|
|
pokeball: pokemon.pokeball,
|
|
canMegaEvo: !!pokemon.canMegaEvo
|
|
});
|
|
}
|
|
return data;
|
|
};
|
|
BattleSide.prototype.randomActive = function () {
|
|
let actives = this.active.filter(function (active) {
|
|
return active && !active.fainted;
|
|
});
|
|
if (!actives.length) return null;
|
|
let i = Math.floor(Math.random() * actives.length);
|
|
return actives[i];
|
|
};
|
|
BattleSide.prototype.addSideCondition = function (status, source, sourceEffect) {
|
|
status = this.battle.getEffect(status);
|
|
if (this.sideConditions[status.id]) {
|
|
if (!status.onRestart) return false;
|
|
return this.battle.singleEvent('Restart', status, this.sideConditions[status.id], this, source, sourceEffect);
|
|
}
|
|
this.sideConditions[status.id] = {id: status.id};
|
|
this.sideConditions[status.id].target = this;
|
|
if (source) {
|
|
this.sideConditions[status.id].source = source;
|
|
this.sideConditions[status.id].sourcePosition = source.position;
|
|
}
|
|
if (status.duration) {
|
|
this.sideConditions[status.id].duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
if (!this.battle.singleEvent('Start', status, this.sideConditions[status.id], this, source, sourceEffect)) {
|
|
delete this.sideConditions[status.id];
|
|
return false;
|
|
}
|
|
this.battle.update();
|
|
return true;
|
|
};
|
|
BattleSide.prototype.getSideCondition = function (status) {
|
|
status = this.battle.getEffect(status);
|
|
if (!this.sideConditions[status.id]) return null;
|
|
return status;
|
|
};
|
|
BattleSide.prototype.removeSideCondition = function (status) {
|
|
status = this.battle.getEffect(status);
|
|
if (!this.sideConditions[status.id]) return false;
|
|
this.battle.singleEvent('End', status, this.sideConditions[status.id], this);
|
|
delete this.sideConditions[status.id];
|
|
this.battle.update();
|
|
return true;
|
|
};
|
|
BattleSide.prototype.send = function () {
|
|
let parts = Array.prototype.slice.call(arguments);
|
|
let functions = parts.map(function (part) {
|
|
return typeof part === 'function';
|
|
});
|
|
let sideUpdate = [];
|
|
if (functions.indexOf(true) < 0) {
|
|
sideUpdate.push('|' + parts.join('|'));
|
|
} else {
|
|
let line = '';
|
|
for (let j = 0; j < parts.length; ++j) {
|
|
line += '|';
|
|
if (functions[j]) {
|
|
line += parts[j](this);
|
|
} else {
|
|
line += parts[j];
|
|
}
|
|
}
|
|
sideUpdate.push(line);
|
|
}
|
|
this.battle.send('sideupdate', this.id + "\n" + sideUpdate);
|
|
};
|
|
BattleSide.prototype.emitCallback = function () {
|
|
this.battle.send('callback', this.id + "\n" +
|
|
Array.prototype.slice.call(arguments).join('|'));
|
|
};
|
|
BattleSide.prototype.emitRequest = function (update) {
|
|
this.battle.send('request', this.id + "\n" + this.battle.rqid + "\n" + JSON.stringify(update));
|
|
};
|
|
BattleSide.prototype.resolveDecision = function () {
|
|
if (this.decision) {
|
|
if (this.decision === true) this.choice = '';
|
|
return;
|
|
}
|
|
|
|
let decisions = [];
|
|
|
|
switch (this.currentRequest) {
|
|
case 'move':
|
|
for (let i = 0; i < this.active.length; i++) {
|
|
let pokemon = this.active[i];
|
|
if (!pokemon || pokemon.fainted) continue;
|
|
|
|
let lockedMove = pokemon.getLockedMove();
|
|
if (lockedMove) {
|
|
decisions.push({
|
|
choice: 'move',
|
|
pokemon: pokemon,
|
|
targetLoc: this.battle.runEvent('LockMoveTarget', pokemon) || 0,
|
|
move: lockedMove
|
|
});
|
|
continue;
|
|
}
|
|
|
|
let moveid = 'struggle';
|
|
let moves = pokemon.getMoves();
|
|
for (let j = 0; j < moves.length; j++) {
|
|
if (moves[j].disabled) continue;
|
|
moveid = moves[j].id;
|
|
break;
|
|
}
|
|
decisions.push({
|
|
choice: 'move',
|
|
pokemon: pokemon,
|
|
targetLoc: 0,
|
|
move: moveid
|
|
});
|
|
}
|
|
break;
|
|
|
|
case 'switch': {
|
|
let canSwitchOut = [];
|
|
for (let i = 0; i < this.active.length; i++) {
|
|
if (this.active[i] && this.active[i].switchFlag) canSwitchOut.push(i);
|
|
}
|
|
|
|
let canSwitchIn = [];
|
|
for (let i = this.active.length; i < this.pokemon.length; i++) {
|
|
if (this.pokemon[i] && !this.pokemon[i].fainted) canSwitchIn.push(i);
|
|
}
|
|
|
|
let willPass = canSwitchOut.splice(Math.min(canSwitchOut.length, canSwitchIn.length));
|
|
for (let i = 0; i < canSwitchOut.length; i++) {
|
|
decisions.push({
|
|
choice: this.foe.currentRequest === 'switch' ? 'instaswitch' : 'switch',
|
|
pokemon: this.active[canSwitchOut[i]],
|
|
target: this.pokemon[canSwitchIn[i]]
|
|
});
|
|
}
|
|
for (let i = 0; i < willPass.length; i++) {
|
|
decisions.push({
|
|
choice: 'pass',
|
|
pokemon: this.active[willPass[i]],
|
|
priority: 102
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'teampreview':
|
|
decisions.push({
|
|
choice: 'team',
|
|
side: this,
|
|
team: [0, 1, 2, 3, 4, 5].slice(0, this.pokemon.length)
|
|
});
|
|
}
|
|
|
|
this.choice = '';
|
|
this.decision = decisions;
|
|
};
|
|
BattleSide.prototype.destroy = function () {
|
|
// deallocate ourself
|
|
|
|
// deallocate children and get rid of references to them
|
|
for (let i = 0; i < this.pokemon.length; i++) {
|
|
if (this.pokemon[i]) this.pokemon[i].destroy();
|
|
this.pokemon[i] = null;
|
|
}
|
|
this.pokemon = null;
|
|
for (let i = 0; i < this.active.length; i++) {
|
|
this.active[i] = null;
|
|
}
|
|
this.active = null;
|
|
|
|
if (this.decision) {
|
|
delete this.decision.side;
|
|
delete this.decision.pokemon;
|
|
}
|
|
this.decision = null;
|
|
|
|
// get rid of some possibly-circular references
|
|
this.battle = null;
|
|
this.foe = null;
|
|
};
|
|
return BattleSide;
|
|
})();
|
|
|
|
Battle = (function () {
|
|
let Battle = {};
|
|
|
|
Battle.construct = (function () {
|
|
let battleProtoCache = {};
|
|
return function (roomid, formatarg, rated) {
|
|
let battle = Object.create((function () {
|
|
if (battleProtoCache[formatarg] !== undefined) {
|
|
return battleProtoCache[formatarg];
|
|
}
|
|
|
|
// Scripts overrides Battle overrides Scripts overrides Tools
|
|
let tools = Tools.mod(formatarg);
|
|
let proto = Object.create(tools);
|
|
for (let i in Battle.prototype) {
|
|
proto[i] = Battle.prototype[i];
|
|
}
|
|
let battle = Object.create(proto);
|
|
tools.install(battle);
|
|
return (battleProtoCache[formatarg] = battle);
|
|
})());
|
|
Battle.prototype.init.call(battle, roomid, formatarg, rated);
|
|
return battle;
|
|
};
|
|
})();
|
|
|
|
Battle.logReplay = function (data, isReplay) {
|
|
if (isReplay === true) return data;
|
|
return '';
|
|
};
|
|
|
|
Battle.prototype = {};
|
|
|
|
Battle.prototype.init = function (roomid, formatarg, rated) {
|
|
let format = Tools.getFormat(formatarg);
|
|
|
|
this.log = [];
|
|
this.sides = [null, null];
|
|
this.roomid = roomid;
|
|
this.id = roomid;
|
|
this.rated = rated;
|
|
this.weatherData = {id:''};
|
|
this.terrainData = {id:''};
|
|
this.pseudoWeather = {};
|
|
|
|
this.format = toId(format);
|
|
this.formatData = {id:this.format};
|
|
|
|
this.effect = {id:''};
|
|
this.effectData = {id:''};
|
|
this.event = {id:''};
|
|
|
|
this.gameType = (format.gameType || 'singles');
|
|
|
|
this.queue = [];
|
|
this.faintQueue = [];
|
|
this.messageLog = [];
|
|
|
|
// use a random initial seed (64-bit, [high -> low])
|
|
this.startingSeed = this.seed = [
|
|
Math.floor(Math.random() * 0x10000),
|
|
Math.floor(Math.random() * 0x10000),
|
|
Math.floor(Math.random() * 0x10000),
|
|
Math.floor(Math.random() * 0x10000)
|
|
];
|
|
};
|
|
|
|
Battle.prototype.turn = 0;
|
|
Battle.prototype.p1 = null;
|
|
Battle.prototype.p2 = null;
|
|
Battle.prototype.lastUpdate = 0;
|
|
Battle.prototype.weather = '';
|
|
Battle.prototype.terrain = '';
|
|
Battle.prototype.ended = false;
|
|
Battle.prototype.started = false;
|
|
Battle.prototype.active = false;
|
|
Battle.prototype.eventDepth = 0;
|
|
Battle.prototype.lastMove = '';
|
|
Battle.prototype.activeMove = null;
|
|
Battle.prototype.activePokemon = null;
|
|
Battle.prototype.activeTarget = null;
|
|
Battle.prototype.midTurn = false;
|
|
Battle.prototype.currentRequest = '';
|
|
Battle.prototype.currentRequestDetails = '';
|
|
Battle.prototype.rqid = 0;
|
|
Battle.prototype.lastMoveLine = 0;
|
|
Battle.prototype.reportPercentages = false;
|
|
Battle.prototype.supportCancel = false;
|
|
Battle.prototype.events = null;
|
|
|
|
Battle.prototype.toString = function () {
|
|
return 'Battle: ' + this.format;
|
|
};
|
|
|
|
// This function is designed to emulate the on-cartridge PRNG for Gens 3 and 4, as described in
|
|
// http://www.smogon.com/ingame/rng/pid_iv_creation#pokemon_random_number_generator
|
|
// This RNG uses a 32-bit initial seed
|
|
|
|
// This function has three different results, depending on arguments:
|
|
// - random() returns a real number in [0, 1), just like Math.random()
|
|
// - random(n) returns an integer in [0, n)
|
|
// - random(m, n) returns an integer in [m, n)
|
|
|
|
// m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
|
|
/*
|
|
Battle.prototype.random = function (m, n) {
|
|
this.seed = (this.seed * 0x41C64E6D + 0x6073) >>> 0; // truncate the result to the last 32 bits
|
|
let result = this.seed >>> 16; // the first 16 bits of the seed are the random value
|
|
m = Math.floor(m);
|
|
n = Math.floor(n);
|
|
return (m ? (n ? (result % (n - m)) + m : result % m) : result / 0x10000);
|
|
};
|
|
*/
|
|
|
|
// This function is designed to emulate the on-cartridge PRNG for Gen 5 and uses a 64-bit initial seed
|
|
|
|
// This function has three different results, depending on arguments:
|
|
// - random() returns a real number in [0, 1), just like Math.random()
|
|
// - random(n) returns an integer in [0, n)
|
|
// - random(m, n) returns an integer in [m, n)
|
|
|
|
// m and n are converted to integers via Math.floor. If the result is NaN, they are ignored.
|
|
|
|
Battle.prototype.random = function (m, n) {
|
|
this.seed = this.nextFrame(); // Advance the RNG
|
|
let result = (this.seed[0] << 16 >>> 0) + this.seed[1]; // Use the upper 32 bits
|
|
m = Math.floor(m);
|
|
n = Math.floor(n);
|
|
result = (m ? (n ? Math.floor(result * (n - m) / 0x100000000) + m : Math.floor(result * m / 0x100000000)) : result / 0x100000000);
|
|
this.debug('randBW(' + (m ? (n ? m + ', ' + n : m) : '') + ') = ' + result);
|
|
return result;
|
|
};
|
|
|
|
Battle.prototype.nextFrame = function (n) {
|
|
let seed = this.seed;
|
|
n = n || 1;
|
|
for (let frame = 0; frame < n; ++frame) {
|
|
// The RNG is a Linear Congruential Generator (LCG) in the form: x_{n + 1} = (a x_n + c) % m
|
|
// Where: x_0 is the seed, x_n is the random number after n iterations,
|
|
// a = 0x5D588B656C078965, c = 0x00269EC3 and m = 2^64
|
|
// Javascript doesnt handle such large numbers properly, so this function does it in 16-bit parts.
|
|
// x_{n + 1} = (x_n * a) + c
|
|
// Let any 64 bit number n = (n[0] << 48) + (n[1] << 32) + (n[2] << 16) + n[3]
|
|
// Then x_{n + 1} =
|
|
// ((a[3] x_n[0] + a[2] x_n[1] + a[1] x_n[2] + a[0] x_n[3] + c[0]) << 48) +
|
|
// ((a[3] x_n[1] + a[2] x_n[2] + a[1] x_n[3] + c[1]) << 32) +
|
|
// ((a[3] x_n[2] + a[2] x_n[3] + c[2]) << 16) +
|
|
// a[3] x_n[3] + c[3]
|
|
// Which can be generalised where b is the number of 16 bit words in the number:
|
|
// (Notice how the a[] word starts at b-1, and decrements every time it appears again on the line;
|
|
// x_n[] starts at b-<line#>-1 and increments to b-1 at the end of the line per line, limiting the length of the line;
|
|
// c[] is at b-<line#>-1 for each line and the left shift is 16 * <line#>)
|
|
// ((a[b-1] + x_n[b-1] + c[b-1]) << (16 * 0)) +
|
|
// ((a[b-1] x_n[b-2] + a[b-2] x_n[b-1] + c[b-2]) << (16 * 1)) +
|
|
// ((a[b-1] x_n[b-3] + a[b-2] x_n[b-2] + a[b-3] x_n[b-1] + c[b-3]) << (16 * 2)) +
|
|
// ...
|
|
// ((a[b-1] x_n[1] + a[b-2] x_n[2] + ... + a[2] x_n[b-2] + a[1] + x_n[b-1] + c[1]) << (16 * (b-2))) +
|
|
// ((a[b-1] x_n[0] + a[b-2] x_n[1] + ... + a[1] x_n[b-2] + a[0] + x_n[b-1] + c[0]) << (16 * (b-1)))
|
|
// Which produces this equation: \sum_{l=0}^{b-1}\left(\sum_{m=b-l-1}^{b-1}\left\{a[2b-m-l-2] x_n[m]\right\}+c[b-l-1]\ll16l\right)
|
|
// This is all ignoring overflow/carry because that cannot be shown in a pseudo-mathematical equation.
|
|
// The below code implements a optimised version of that equation while also checking for overflow/carry.
|
|
|
|
let a = [0x5D58, 0x8B65, 0x6C07, 0x8965];
|
|
let c = [0, 0, 0x26, 0x9EC3];
|
|
|
|
let nextSeed = [0, 0, 0, 0];
|
|
let carry = 0;
|
|
|
|
for (let cN = seed.length - 1; cN >= 0; --cN) {
|
|
nextSeed[cN] = carry;
|
|
carry = 0;
|
|
|
|
let aN = seed.length - 1;
|
|
let seedN = cN;
|
|
for (; seedN < seed.length; --aN, ++seedN) {
|
|
let nextWord = a[aN] * seed[seedN];
|
|
carry += nextWord >>> 16;
|
|
nextSeed[cN] += nextWord & 0xFFFF;
|
|
}
|
|
nextSeed[cN] += c[cN];
|
|
carry += nextSeed[cN] >>> 16;
|
|
nextSeed[cN] &= 0xFFFF;
|
|
}
|
|
|
|
seed = nextSeed;
|
|
}
|
|
return seed;
|
|
};
|
|
|
|
Battle.prototype.setWeather = function (status, source, sourceEffect) {
|
|
status = this.getEffect(status);
|
|
if (sourceEffect === undefined && this.effect) sourceEffect = this.effect;
|
|
if (source === undefined && this.event && this.event.target) source = this.event.target;
|
|
|
|
if (this.weather === status.id && (this.gen > 2 || status.id === 'sandstorm')) {
|
|
return false;
|
|
}
|
|
if (status.id) {
|
|
let result = this.runEvent('SetWeather', source, source, status);
|
|
if (!result) {
|
|
if (result === false) {
|
|
if (sourceEffect && sourceEffect.weather) {
|
|
this.add('-fail', source, sourceEffect, '[from]: ' + this.weather);
|
|
} else if (sourceEffect && sourceEffect.effectType === 'Ability') {
|
|
this.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]');
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
if (this.weather && !status.id) {
|
|
let oldstatus = this.getWeather();
|
|
this.singleEvent('End', oldstatus, this.weatherData, this);
|
|
}
|
|
let prevWeather = this.weather;
|
|
let prevWeatherData = this.weatherData;
|
|
this.weather = status.id;
|
|
this.weatherData = {id: status.id};
|
|
if (source) {
|
|
this.weatherData.source = source;
|
|
this.weatherData.sourcePosition = source.position;
|
|
}
|
|
if (status.duration) {
|
|
this.weatherData.duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.weatherData.duration = status.durationCallback.call(this, source, sourceEffect);
|
|
}
|
|
if (!this.singleEvent('Start', status, this.weatherData, this, source, sourceEffect)) {
|
|
this.weather = prevWeather;
|
|
this.weatherData = prevWeatherData;
|
|
return false;
|
|
}
|
|
this.update();
|
|
return true;
|
|
};
|
|
Battle.prototype.clearWeather = function () {
|
|
return this.setWeather('');
|
|
};
|
|
Battle.prototype.effectiveWeather = function (target) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
}
|
|
if (this.suppressingWeather()) return '';
|
|
return this.weather;
|
|
};
|
|
Battle.prototype.isWeather = function (weather, target) {
|
|
let ourWeather = this.effectiveWeather(target);
|
|
if (!Array.isArray(weather)) {
|
|
return ourWeather === toId(weather);
|
|
}
|
|
return (weather.map(toId).indexOf(ourWeather) >= 0);
|
|
};
|
|
Battle.prototype.getWeather = function () {
|
|
return this.getEffect(this.weather);
|
|
};
|
|
|
|
Battle.prototype.setTerrain = function (status, source, sourceEffect) {
|
|
status = this.getEffect(status);
|
|
if (sourceEffect === undefined && this.effect) sourceEffect = this.effect;
|
|
if (source === undefined && this.event && this.event.target) source = this.event.target;
|
|
|
|
if (this.terrain === status.id) return false;
|
|
if (this.terrain && !status.id) {
|
|
let oldstatus = this.getTerrain();
|
|
this.singleEvent('End', oldstatus, this.terrainData, this);
|
|
}
|
|
let prevTerrain = this.terrain;
|
|
let prevTerrainData = this.terrainData;
|
|
this.terrain = status.id;
|
|
this.terrainData = {id: status.id};
|
|
if (source) {
|
|
this.terrainData.source = source;
|
|
this.terrainData.sourcePosition = source.position;
|
|
}
|
|
if (status.duration) {
|
|
this.terrainData.duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.terrainData.duration = status.durationCallback.call(this, source, sourceEffect);
|
|
}
|
|
if (!this.singleEvent('Start', status, this.terrainData, this, source, sourceEffect)) {
|
|
this.terrain = prevTerrain;
|
|
this.terrainData = prevTerrainData;
|
|
return false;
|
|
}
|
|
this.update();
|
|
return true;
|
|
};
|
|
Battle.prototype.clearTerrain = function () {
|
|
return this.setTerrain('');
|
|
};
|
|
Battle.prototype.effectiveTerrain = function (target) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
}
|
|
if (!this.runEvent('TryTerrain', target)) return '';
|
|
return this.terrain;
|
|
};
|
|
Battle.prototype.isTerrain = function (terrain, target) {
|
|
let ourTerrain = this.effectiveTerrain(target);
|
|
if (!Array.isArray(terrain)) {
|
|
return ourTerrain === toId(terrain);
|
|
}
|
|
return (terrain.map(toId).indexOf(ourTerrain) >= 0);
|
|
};
|
|
Battle.prototype.getTerrain = function () {
|
|
return this.getEffect(this.terrain);
|
|
};
|
|
|
|
Battle.prototype.getFormat = function () {
|
|
return this.getEffect(this.format);
|
|
};
|
|
Battle.prototype.addPseudoWeather = function (status, source, sourceEffect) {
|
|
status = this.getEffect(status);
|
|
if (this.pseudoWeather[status.id]) {
|
|
if (!status.onRestart) return false;
|
|
return this.singleEvent('Restart', status, this.pseudoWeather[status.id], this, source, sourceEffect);
|
|
}
|
|
this.pseudoWeather[status.id] = {id: status.id};
|
|
if (source) {
|
|
this.pseudoWeather[status.id].source = source;
|
|
this.pseudoWeather[status.id].sourcePosition = source.position;
|
|
}
|
|
if (status.duration) {
|
|
this.pseudoWeather[status.id].duration = status.duration;
|
|
}
|
|
if (status.durationCallback) {
|
|
this.pseudoWeather[status.id].duration = status.durationCallback.call(this, source, sourceEffect);
|
|
}
|
|
if (!this.singleEvent('Start', status, this.pseudoWeather[status.id], this, source, sourceEffect)) {
|
|
delete this.pseudoWeather[status.id];
|
|
return false;
|
|
}
|
|
this.update();
|
|
return true;
|
|
};
|
|
Battle.prototype.getPseudoWeather = function (status) {
|
|
status = this.getEffect(status);
|
|
if (!this.pseudoWeather[status.id]) return null;
|
|
return status;
|
|
};
|
|
Battle.prototype.removePseudoWeather = function (status) {
|
|
status = this.getEffect(status);
|
|
if (!this.pseudoWeather[status.id]) return false;
|
|
this.singleEvent('End', status, this.pseudoWeather[status.id], this);
|
|
delete this.pseudoWeather[status.id];
|
|
this.update();
|
|
return true;
|
|
};
|
|
Battle.prototype.suppressingAttackEvents = function () {
|
|
return (this.activePokemon && this.activePokemon.isActive && !this.activePokemon.ignoringAbility() && this.activePokemon.getAbility().stopAttackEvents);
|
|
};
|
|
Battle.prototype.suppressingWeather = function () {
|
|
let pokemon;
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
for (let j = 0; j < this.sides[i].active.length; j++) {
|
|
pokemon = this.sides[i].active[j];
|
|
if (pokemon && !pokemon.ignoringAbility() && pokemon.getAbility().suppressWeather) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
Battle.prototype.setActiveMove = function (move, pokemon, target) {
|
|
if (!move) move = null;
|
|
if (!pokemon) pokemon = null;
|
|
if (!target) target = pokemon;
|
|
this.activeMove = move;
|
|
this.activePokemon = pokemon;
|
|
this.activeTarget = target;
|
|
|
|
// Mold Breaker and the like
|
|
this.update();
|
|
};
|
|
Battle.prototype.clearActiveMove = function (failed) {
|
|
if (this.activeMove) {
|
|
if (!failed) {
|
|
this.lastMove = this.activeMove.id;
|
|
}
|
|
this.activeMove = null;
|
|
this.activePokemon = null;
|
|
this.activeTarget = null;
|
|
|
|
// Mold Breaker and the like, again
|
|
this.update();
|
|
}
|
|
};
|
|
|
|
Battle.prototype.update = function () {
|
|
let actives = this.p1.active;
|
|
for (let i = 0; i < actives.length; i++) {
|
|
if (actives[i]) actives[i].update();
|
|
}
|
|
actives = this.p2.active;
|
|
for (let i = 0; i < actives.length; i++) {
|
|
if (actives[i]) actives[i].update();
|
|
}
|
|
};
|
|
|
|
// bubbles up
|
|
Battle.comparePriority = function (a, b) { // intentionally not in Battle.prototype
|
|
a.priority = a.priority || 0;
|
|
a.subPriority = a.subPriority || 0;
|
|
a.speed = a.speed || 0;
|
|
b.priority = b.priority || 0;
|
|
b.subPriority = b.subPriority || 0;
|
|
b.speed = b.speed || 0;
|
|
if ((typeof a.order === 'number' || typeof b.order === 'number') && a.order !== b.order) {
|
|
if (typeof a.order !== 'number') {
|
|
return -1;
|
|
}
|
|
if (typeof b.order !== 'number') {
|
|
return 1;
|
|
}
|
|
if (b.order - a.order) {
|
|
return -(b.order - a.order);
|
|
}
|
|
}
|
|
if (b.priority - a.priority) {
|
|
return b.priority - a.priority;
|
|
}
|
|
if (b.speed - a.speed) {
|
|
return b.speed - a.speed;
|
|
}
|
|
if (b.subOrder - a.subOrder) {
|
|
return -(b.subOrder - a.subOrder);
|
|
}
|
|
return Math.random() - 0.5;
|
|
};
|
|
Battle.prototype.getResidualStatuses = function (thing, callbackType) {
|
|
let statuses = this.getRelevantEffectsInner(thing || this, callbackType || 'residualCallback', null, null, false, true, 'duration');
|
|
statuses.sort(Battle.comparePriority);
|
|
//if (statuses[0]) this.debug('match ' + (callbackType || 'residualCallback') + ': ' + statuses[0].status.id);
|
|
return statuses;
|
|
};
|
|
Battle.prototype.eachEvent = function (eventid, effect, relayVar) {
|
|
let actives = [];
|
|
if (!effect && this.effect) effect = this.effect;
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
let side = this.sides[i];
|
|
for (let j = 0; j < side.active.length; j++) {
|
|
if (side.active[j]) actives.push(side.active[j]);
|
|
}
|
|
}
|
|
actives.sort(function (a, b) {
|
|
if (b.speed - a.speed) {
|
|
return b.speed - a.speed;
|
|
}
|
|
return Math.random() - 0.5;
|
|
});
|
|
for (let i = 0; i < actives.length; i++) {
|
|
if (actives[i].isStarted) {
|
|
this.runEvent(eventid, actives[i], null, effect, relayVar);
|
|
}
|
|
}
|
|
};
|
|
Battle.prototype.residualEvent = function (eventid, relayVar) {
|
|
let statuses = this.getRelevantEffectsInner(this, 'on' + eventid, null, null, false, true, 'duration');
|
|
statuses.sort(Battle.comparePriority);
|
|
while (statuses.length) {
|
|
let statusObj = statuses.shift();
|
|
let status = statusObj.status;
|
|
if (statusObj.thing.fainted) continue;
|
|
if (statusObj.statusData && statusObj.statusData.duration) {
|
|
statusObj.statusData.duration--;
|
|
if (!statusObj.statusData.duration) {
|
|
statusObj.end.call(statusObj.thing, status.id);
|
|
continue;
|
|
}
|
|
}
|
|
this.singleEvent(eventid, status, statusObj.statusData, statusObj.thing, relayVar);
|
|
}
|
|
};
|
|
// The entire event system revolves around this function
|
|
// (and its helper functions, getRelevant * )
|
|
Battle.prototype.singleEvent = function (eventid, effect, effectData, target, source, sourceEffect, relayVar) {
|
|
if (this.eventDepth >= 8) {
|
|
// oh fuck
|
|
this.add('message', 'STACK LIMIT EXCEEDED');
|
|
this.add('message', 'PLEASE REPORT IN BUG THREAD');
|
|
this.add('message', 'Event: ' + eventid);
|
|
this.add('message', 'Parent event: ' + this.event.id);
|
|
throw new Error("Stack overflow");
|
|
}
|
|
//this.add('Event: ' + eventid + ' (depth ' + this.eventDepth + ')');
|
|
effect = this.getEffect(effect);
|
|
let hasRelayVar = true;
|
|
if (relayVar === undefined) {
|
|
relayVar = true;
|
|
hasRelayVar = false;
|
|
}
|
|
|
|
if (effect.effectType === 'Status' && target.status !== effect.id) {
|
|
// it's changed; call it off
|
|
return relayVar;
|
|
}
|
|
if (eventid !== 'Start' && eventid !== 'TakeItem' && effect.effectType === 'Item' && (target instanceof BattlePokemon) && target.ignoringItem()) {
|
|
this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room');
|
|
return relayVar;
|
|
}
|
|
if (eventid !== 'End' && effect.effectType === 'Ability' && (target instanceof BattlePokemon) && target.ignoringAbility()) {
|
|
this.debug(eventid + ' handler suppressed by Gastro Acid');
|
|
return relayVar;
|
|
}
|
|
if (effect.effectType === 'Weather' && eventid !== 'Start' && eventid !== 'Residual' && eventid !== 'End' && this.suppressingWeather()) {
|
|
this.debug(eventid + ' handler suppressed by Air Lock');
|
|
return relayVar;
|
|
}
|
|
|
|
if (effect['on' + eventid] === undefined) return relayVar;
|
|
let parentEffect = this.effect;
|
|
let parentEffectData = this.effectData;
|
|
let parentEvent = this.event;
|
|
this.effect = effect;
|
|
this.effectData = effectData;
|
|
this.event = {id: eventid, target: target, source: source, effect: sourceEffect};
|
|
this.eventDepth++;
|
|
let args = [target, source, sourceEffect];
|
|
if (hasRelayVar) args.unshift(relayVar);
|
|
let returnVal;
|
|
if (typeof effect['on' + eventid] === 'function') {
|
|
returnVal = effect['on' + eventid].apply(this, args);
|
|
} else {
|
|
returnVal = effect['on' + eventid];
|
|
}
|
|
this.eventDepth--;
|
|
this.effect = parentEffect;
|
|
this.effectData = parentEffectData;
|
|
this.event = parentEvent;
|
|
if (returnVal === undefined) return relayVar;
|
|
return returnVal;
|
|
};
|
|
/**
|
|
* runEvent is the core of Pokemon Showdown's event system.
|
|
*
|
|
* Basic usage
|
|
* ===========
|
|
*
|
|
* this.runEvent('Blah')
|
|
* will trigger any onBlah global event handlers.
|
|
*
|
|
* this.runEvent('Blah', target)
|
|
* will additionally trigger any onBlah handlers on the target, onAllyBlah
|
|
* handlers on any active pokemon on the target's team, and onFoeBlah
|
|
* handlers on any active pokemon on the target's foe's team
|
|
*
|
|
* this.runEvent('Blah', target, source)
|
|
* will additionally trigger any onSourceBlah handlers on the source
|
|
*
|
|
* this.runEvent('Blah', target, source, effect)
|
|
* will additionally pass the effect onto all event handlers triggered
|
|
*
|
|
* this.runEvent('Blah', target, source, effect, relayVar)
|
|
* will additionally pass the relayVar as the first argument along all event
|
|
* handlers
|
|
*
|
|
* You may leave any of these null. For instance, if you have a relayVar but
|
|
* no source or effect:
|
|
* this.runEvent('Damage', target, null, null, 50)
|
|
*
|
|
* Event handlers
|
|
* ==============
|
|
*
|
|
* Items, abilities, statuses, and other effects like SR, confusion, weather,
|
|
* or Trick Room can have event handlers. Event handlers are functions that
|
|
* can modify what happens during an event.
|
|
*
|
|
* event handlers are passed:
|
|
* function (target, source, effect)
|
|
* although some of these can be blank.
|
|
*
|
|
* certain events have a relay variable, in which case they're passed:
|
|
* function (relayVar, target, source, effect)
|
|
*
|
|
* Relay variables are variables that give additional information about the
|
|
* event. For instance, the damage event has a relayVar which is the amount
|
|
* of damage dealt.
|
|
*
|
|
* If a relay variable isn't passed to runEvent, there will still be a secret
|
|
* relayVar defaulting to `true`, but it won't get passed to any event
|
|
* handlers.
|
|
*
|
|
* After an event handler is run, its return value helps determine what
|
|
* happens next:
|
|
* 1. If the return value isn't `undefined`, relayVar is set to the return
|
|
* value
|
|
* 2. If relayVar is falsy, no more event handlers are run
|
|
* 3. Otherwise, if there are more event handlers, the next one is run and
|
|
* we go back to step 1.
|
|
* 4. Once all event handlers are run (or one of them results in a falsy
|
|
* relayVar), relayVar is returned by runEvent
|
|
*
|
|
* As a shortcut, an event handler that isn't a function will be interpreted
|
|
* as a function that returns that value.
|
|
*
|
|
* You can have return values mean whatever you like, but in general, we
|
|
* follow the convention that returning `false` or `null` means
|
|
* stopping or interrupting the event.
|
|
*
|
|
* For instance, returning `false` from a TrySetStatus handler means that
|
|
* the pokemon doesn't get statused.
|
|
*
|
|
* If a failed event usually results in a message like "But it failed!"
|
|
* or "It had no effect!", returning `null` will suppress that message and
|
|
* returning `false` will display it. Returning `null` is useful if your
|
|
* event handler already gave its own custom failure message.
|
|
*
|
|
* Returning `undefined` means "don't change anything" or "keep going".
|
|
* A function that does nothing but return `undefined` is the equivalent
|
|
* of not having an event handler at all.
|
|
*
|
|
* Returning a value means that that value is the new `relayVar`. For
|
|
* instance, if a Damage event handler returns 50, the damage event
|
|
* will deal 50 damage instead of whatever it was going to deal before.
|
|
*
|
|
* Useful values
|
|
* =============
|
|
*
|
|
* In addition to all the methods and attributes of Tools, Battle, and
|
|
* Scripts, event handlers have some additional values they can access:
|
|
*
|
|
* this.effect:
|
|
* the Effect having the event handler
|
|
* this.effectData:
|
|
* the data store associated with the above Effect. This is a plain Object
|
|
* and you can use it to store data for later event handlers.
|
|
* this.effectData.target:
|
|
* the Pokemon, Side, or Battle that the event handler's effect was
|
|
* attached to.
|
|
* this.event.id:
|
|
* the event ID
|
|
* this.event.target, this.event.source, this.event.effect:
|
|
* the target, source, and effect of the event. These are the same
|
|
* variables that are passed as arguments to the event handler, but
|
|
* they're useful for functions called by the event handler.
|
|
*/
|
|
Battle.prototype.runEvent = function (eventid, target, source, effect, relayVar, onEffect) {
|
|
if (this.eventDepth >= 8) {
|
|
// oh fuck
|
|
this.add('message', 'STACK LIMIT EXCEEDED');
|
|
this.add('message', 'PLEASE REPORT IN BUG THREAD');
|
|
this.add('message', 'Event: ' + eventid);
|
|
this.add('message', 'Parent event: ' + this.event.id);
|
|
throw new Error("Stack overflow");
|
|
}
|
|
if (!target) target = this;
|
|
let statuses = this.getRelevantEffects(target, 'on' + eventid, 'onSource' + eventid, source);
|
|
let hasRelayVar = true;
|
|
effect = this.getEffect(effect);
|
|
let args = [target, source, effect];
|
|
//console.log('Event: ' + eventid + ' (depth ' + this.eventDepth + ') t:' + target.id + ' s:' + (!source || source.id) + ' e:' + effect.id);
|
|
if (relayVar === undefined || relayVar === null) {
|
|
relayVar = true;
|
|
hasRelayVar = false;
|
|
} else {
|
|
args.unshift(relayVar);
|
|
}
|
|
|
|
let parentEvent = this.event;
|
|
this.event = {id: eventid, target: target, source: source, effect: effect, modifier: 1};
|
|
this.eventDepth++;
|
|
|
|
if (onEffect && 'on' + eventid in effect) {
|
|
statuses.unshift({status: effect, callback: effect['on' + eventid], statusData: {}, end: null, thing: target});
|
|
}
|
|
for (let i = 0; i < statuses.length; i++) {
|
|
let status = statuses[i].status;
|
|
let thing = statuses[i].thing;
|
|
//this.debug('match ' + eventid + ': ' + status.id + ' ' + status.effectType);
|
|
if (status.effectType === 'Status' && thing.status !== status.id) {
|
|
// it's changed; call it off
|
|
continue;
|
|
}
|
|
if (status.effectType === 'Ability' && this.suppressingAttackEvents() && this.activePokemon !== thing) {
|
|
// ignore attacking events
|
|
let AttackingEvents = {
|
|
BeforeMove: 1,
|
|
BasePower: 1,
|
|
Immunity: 1,
|
|
RedirectTarget: 1,
|
|
Heal: 1,
|
|
SetStatus: 1,
|
|
CriticalHit: 1,
|
|
ModifyPokemon: 1,
|
|
ModifyAtk: 1, ModifyDef: 1, ModifySpA: 1, ModifySpD: 1, ModifySpe: 1, ModifyAccuracy: 1,
|
|
ModifyBoost: 1,
|
|
ModifyDamage: 1,
|
|
ModifySecondaries: 1,
|
|
ModifyWeight: 1,
|
|
TryHit: 1,
|
|
TryHitSide: 1,
|
|
TryMove: 1,
|
|
Hit: 1,
|
|
Boost: 1,
|
|
DragOut: 1
|
|
};
|
|
if (eventid in AttackingEvents) {
|
|
if (eventid !== 'ModifyPokemon') {
|
|
this.debug(eventid + ' handler suppressed by Mold Breaker');
|
|
}
|
|
continue;
|
|
} else if (eventid === 'Damage' && effect && effect.effectType === 'Move') {
|
|
this.debug(eventid + ' handler suppressed by Mold Breaker');
|
|
continue;
|
|
}
|
|
}
|
|
if (eventid !== 'Start' && eventid !== 'TakeItem' && status.effectType === 'Item' && (thing instanceof BattlePokemon) && thing.ignoringItem()) {
|
|
if (eventid !== 'ModifyPokemon' && eventid !== 'Update') {
|
|
this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room');
|
|
}
|
|
continue;
|
|
} else if (eventid !== 'End' && status.effectType === 'Ability' && (thing instanceof BattlePokemon) && thing.ignoringAbility()) {
|
|
if (eventid !== 'ModifyPokemon' && eventid !== 'Update') {
|
|
this.debug(eventid + ' handler suppressed by Gastro Acid');
|
|
}
|
|
continue;
|
|
}
|
|
if ((status.effectType === 'Weather' || eventid === 'Weather') && eventid !== 'Residual' && eventid !== 'End' && this.suppressingWeather()) {
|
|
this.debug(eventid + ' handler suppressed by Air Lock');
|
|
continue;
|
|
}
|
|
let returnVal;
|
|
if (typeof statuses[i].callback === 'function') {
|
|
let parentEffect = this.effect;
|
|
let parentEffectData = this.effectData;
|
|
this.effect = statuses[i].status;
|
|
this.effectData = statuses[i].statusData;
|
|
this.effectData.target = thing;
|
|
|
|
returnVal = statuses[i].callback.apply(this, args);
|
|
|
|
this.effect = parentEffect;
|
|
this.effectData = parentEffectData;
|
|
} else {
|
|
returnVal = statuses[i].callback;
|
|
}
|
|
|
|
if (returnVal !== undefined) {
|
|
relayVar = returnVal;
|
|
if (!relayVar) break;
|
|
if (hasRelayVar) {
|
|
args[0] = relayVar;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.eventDepth--;
|
|
if (this.event.modifier !== 1 && typeof relayVar === 'number') {
|
|
// this.debug(eventid + ' modifier: 0x' + ('0000' + (this.event.modifier * 4096).toString(16)).slice(-4).toUpperCase());
|
|
relayVar = this.modify(relayVar, this.event.modifier);
|
|
}
|
|
this.event = parentEvent;
|
|
|
|
return relayVar;
|
|
};
|
|
Battle.prototype.resolveLastPriority = function (statuses, callbackType) {
|
|
let order = false;
|
|
let priority = 0;
|
|
let subOrder = 0;
|
|
let status = statuses[statuses.length - 1];
|
|
if (status.status[callbackType + 'Order']) {
|
|
order = status.status[callbackType + 'Order'];
|
|
}
|
|
if (status.status[callbackType + 'Priority']) {
|
|
priority = status.status[callbackType + 'Priority'];
|
|
} else if (status.status[callbackType + 'SubOrder']) {
|
|
subOrder = status.status[callbackType + 'SubOrder'];
|
|
}
|
|
|
|
status.order = order;
|
|
status.priority = priority;
|
|
status.subOrder = subOrder;
|
|
if (status.thing && status.thing.getStat) status.speed = status.thing.speed;
|
|
};
|
|
// bubbles up to parents
|
|
Battle.prototype.getRelevantEffects = function (thing, callbackType, foeCallbackType, foeThing) {
|
|
let statuses = this.getRelevantEffectsInner(thing, callbackType, foeCallbackType, foeThing, true, false);
|
|
statuses.sort(Battle.comparePriority);
|
|
//if (statuses[0]) this.debug('match ' + callbackType + ': ' + statuses[0].status.id);
|
|
return statuses;
|
|
};
|
|
Battle.prototype.getRelevantEffectsInner = function (thing, callbackType, foeCallbackType, foeThing, bubbleUp, bubbleDown, getAll) {
|
|
if (!callbackType || !thing) return [];
|
|
let statuses = [];
|
|
let status;
|
|
|
|
if (thing.sides) {
|
|
for (let i in this.pseudoWeather) {
|
|
status = this.getPseudoWeather(i);
|
|
if (status[callbackType] !== undefined || (getAll && thing.pseudoWeather[i][getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: this.pseudoWeather[i], end: this.removePseudoWeather, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
}
|
|
status = this.getWeather();
|
|
if (status[callbackType] !== undefined || (getAll && thing.weatherData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: this.weatherData, end: this.clearWeather, thing: thing, priority: status[callbackType + 'Priority'] || 0});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
status = this.getTerrain();
|
|
if (status[callbackType] !== undefined || (getAll && thing.terrainData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: this.terrainData, end: this.clearTerrain, thing: thing, priority: status[callbackType + 'Priority'] || 0});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
status = this.getFormat();
|
|
if (status[callbackType] !== undefined || (getAll && thing.formatData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: this.formatData, end: function () {}, thing: thing, priority: status[callbackType + 'Priority'] || 0});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
if (this.events && this.events[callbackType] !== undefined) {
|
|
for (let i = 0; i < this.events[callbackType].length; i++) {
|
|
let handler = this.events[callbackType][i];
|
|
let statusData;
|
|
switch (handler.target.effectType) {
|
|
case 'Format':
|
|
statusData = this.formatData;
|
|
}
|
|
statuses.push({status: handler.target, callback: handler.callback, statusData: statusData, end: function () {}, thing: thing, priority: handler.priority, order: handler.order, subOrder: handler.subOrder});
|
|
}
|
|
}
|
|
if (bubbleDown) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(this.p1, callbackType, null, null, false, true, getAll));
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(this.p2, callbackType, null, null, false, true, getAll));
|
|
}
|
|
return statuses;
|
|
}
|
|
|
|
if (thing.pokemon) {
|
|
for (let i in thing.sideConditions) {
|
|
status = thing.getSideCondition(i);
|
|
if (status[callbackType] !== undefined || (getAll && thing.sideConditions[i][getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.sideConditions[i], end: thing.removeSideCondition, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
}
|
|
if (foeCallbackType) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(thing.foe, foeCallbackType, null, null, false, false, getAll));
|
|
if (foeCallbackType.substr(0, 5) === 'onFoe') {
|
|
let eventName = foeCallbackType.substr(5);
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(thing.foe, 'onAny' + eventName, null, null, false, false, getAll));
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(thing, 'onAny' + eventName, null, null, false, false, getAll));
|
|
}
|
|
}
|
|
if (bubbleUp) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(this, callbackType, null, null, true, false, getAll));
|
|
}
|
|
if (bubbleDown) {
|
|
for (let i = 0; i < thing.active.length; i++) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(thing.active[i], callbackType, null, null, false, true, getAll));
|
|
}
|
|
}
|
|
return statuses;
|
|
}
|
|
|
|
if (!thing.getStatus) {
|
|
//this.debug(JSON.stringify(thing));
|
|
return statuses;
|
|
}
|
|
status = thing.getStatus();
|
|
if (status[callbackType] !== undefined || (getAll && thing.statusData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.statusData, end: thing.clearStatus, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
for (let i in thing.volatiles) {
|
|
status = thing.getVolatile(i);
|
|
if (status[callbackType] !== undefined || (getAll && thing.volatiles[i][getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.volatiles[i], end: thing.removeVolatile, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
}
|
|
status = thing.getAbility();
|
|
if (status[callbackType] !== undefined || (getAll && thing.abilityData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.abilityData, end: thing.clearAbility, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
status = thing.getItem();
|
|
if (status[callbackType] !== undefined || (getAll && thing.itemData[getAll])) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.itemData, end: thing.clearItem, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
status = this.getEffect(thing.template.baseSpecies);
|
|
if (status[callbackType] !== undefined) {
|
|
statuses.push({status: status, callback: status[callbackType], statusData: thing.speciesData, end: function () {}, thing: thing});
|
|
this.resolveLastPriority(statuses, callbackType);
|
|
}
|
|
|
|
if (foeThing && foeCallbackType && foeCallbackType.substr(0, 8) !== 'onSource') {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(foeThing, foeCallbackType, null, null, false, false, getAll));
|
|
} else if (foeCallbackType) {
|
|
let foeActive = thing.side.foe.active;
|
|
let allyActive = thing.side.active;
|
|
let eventName = '';
|
|
if (foeCallbackType.substr(0, 8) === 'onSource') {
|
|
eventName = foeCallbackType.substr(8);
|
|
if (foeThing) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(foeThing, foeCallbackType, null, null, false, false, getAll));
|
|
}
|
|
foeCallbackType = 'onFoe' + eventName;
|
|
foeThing = null;
|
|
}
|
|
if (foeCallbackType.substr(0, 5) === 'onFoe') {
|
|
eventName = foeCallbackType.substr(5);
|
|
for (let i = 0; i < allyActive.length; i++) {
|
|
if (!allyActive[i] || allyActive[i].fainted) continue;
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(allyActive[i], 'onAlly' + eventName, null, null, false, false, getAll));
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(allyActive[i], 'onAny' + eventName, null, null, false, false, getAll));
|
|
}
|
|
for (let i = 0; i < foeActive.length; i++) {
|
|
if (!foeActive[i] || foeActive[i].fainted) continue;
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(foeActive[i], 'onAny' + eventName, null, null, false, false, getAll));
|
|
}
|
|
}
|
|
for (let i = 0; i < foeActive.length; i++) {
|
|
if (!foeActive[i] || foeActive[i].fainted) continue;
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(foeActive[i], foeCallbackType, null, null, false, false, getAll));
|
|
}
|
|
}
|
|
if (bubbleUp) {
|
|
statuses = statuses.concat(this.getRelevantEffectsInner(thing.side, callbackType, foeCallbackType, null, true, false, getAll));
|
|
}
|
|
return statuses;
|
|
};
|
|
/**
|
|
* Use this function to attach custom event handlers to a battle. See Battle#runEvent for
|
|
* more information on how to write callbacks for event handlers.
|
|
*
|
|
* Try to use this sparingly. Most event handlers can be simply placed in a format instead.
|
|
*
|
|
* this.on(eventid, target, callback)
|
|
* will set the callback as an event handler for the target when eventid is called with the
|
|
* default priority. Currently only valid formats are supported as targets but this will
|
|
* eventually be expanded to support other target types.
|
|
*
|
|
* this.on(eventid, target, priority, callback)
|
|
* will set the callback as an event handler for the target when eventid is called with the
|
|
* provided priority. Priority can either be a number or an object that contains the priority,
|
|
* order, and subOrder for the evend handler as needed (undefined keys will use default values)
|
|
*/
|
|
Battle.prototype.on = function (eventid, target /*[, priority], callback*/) {
|
|
if (!eventid) throw new TypeError("Event handlers must have an event to listen to");
|
|
if (!target) throw new TypeError("Event handlers must have a target");
|
|
if (arguments.length < 3) throw new TypeError("Event handlers must have a callback");
|
|
|
|
if (target.effectType !== 'Format') {
|
|
throw new TypeError("" + target.effectType + " targets are not supported at this time");
|
|
}
|
|
|
|
let callback, priority, order, subOrder;
|
|
if (arguments.length === 3) {
|
|
callback = arguments[2];
|
|
priority = 0;
|
|
order = false;
|
|
subOrder = 0;
|
|
} else {
|
|
callback = arguments[3];
|
|
let data = arguments[2];
|
|
if (typeof data === 'object') {
|
|
priority = data['priority'] || 0;
|
|
order = data['order'] || false;
|
|
subOrder = data['subOrder'] || 0;
|
|
} else {
|
|
priority = data || 0;
|
|
order = false;
|
|
subOrder = 0;
|
|
}
|
|
}
|
|
|
|
let eventHandler = {callback: callback, target: target, priority: priority, order: order, subOrder: subOrder};
|
|
|
|
let callbackType = 'on' + eventid;
|
|
if (!this.events) this.events = {};
|
|
if (this.events[callbackType] === undefined) {
|
|
this.events[callbackType] = [eventHandler];
|
|
} else {
|
|
this.events[callbackType].push(eventHandler);
|
|
}
|
|
};
|
|
Battle.prototype.getPokemon = function (id) {
|
|
if (typeof id !== 'string') id = id.id;
|
|
for (let i = 0; i < this.p1.pokemon.length; i++) {
|
|
let pokemon = this.p1.pokemon[i];
|
|
if (pokemon.id === id) return pokemon;
|
|
}
|
|
for (let i = 0; i < this.p2.pokemon.length; i++) {
|
|
let pokemon = this.p2.pokemon[i];
|
|
if (pokemon.id === id) return pokemon;
|
|
}
|
|
return null;
|
|
};
|
|
Battle.prototype.makeRequest = function (type, requestDetails) {
|
|
if (type) {
|
|
this.currentRequest = type;
|
|
this.currentRequestDetails = requestDetails || '';
|
|
this.rqid++;
|
|
this.p1.decision = null;
|
|
this.p2.decision = null;
|
|
} else {
|
|
type = this.currentRequest;
|
|
requestDetails = this.currentRequestDetails;
|
|
}
|
|
this.update();
|
|
|
|
// default to no request
|
|
let p1request = null;
|
|
let p2request = null;
|
|
this.p1.currentRequest = '';
|
|
this.p2.currentRequest = '';
|
|
let switchTable = [];
|
|
|
|
switch (type) {
|
|
case 'switch': {
|
|
for (let i = 0, l = this.p1.active.length; i < l; i++) {
|
|
let active = this.p1.active[i];
|
|
switchTable.push(!!(active && active.switchFlag));
|
|
}
|
|
if (switchTable.any(true)) {
|
|
this.p1.currentRequest = 'switch';
|
|
p1request = {forceSwitch: switchTable, side: this.p1.getData(), rqid: this.rqid};
|
|
}
|
|
switchTable = [];
|
|
for (let i = 0, l = this.p2.active.length; i < l; i++) {
|
|
let active = this.p2.active[i];
|
|
switchTable.push(!!(active && active.switchFlag));
|
|
}
|
|
if (switchTable.any(true)) {
|
|
this.p2.currentRequest = 'switch';
|
|
p2request = {forceSwitch: switchTable, side: this.p2.getData(), rqid: this.rqid};
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'teampreview':
|
|
this.add('teampreview' + (requestDetails ? '|' + requestDetails : ''));
|
|
this.p1.currentRequest = 'teampreview';
|
|
p1request = {teamPreview: true, side: this.p1.getData(), rqid: this.rqid};
|
|
this.p2.currentRequest = 'teampreview';
|
|
p2request = {teamPreview: true, side: this.p2.getData(), rqid: this.rqid};
|
|
break;
|
|
|
|
default: {
|
|
this.p1.currentRequest = 'move';
|
|
let activeData = this.p1.active.map(function (pokemon) {
|
|
if (pokemon) return pokemon.getRequestData();
|
|
});
|
|
p1request = {active: activeData, side: this.p1.getData(), rqid: this.rqid};
|
|
|
|
this.p2.currentRequest = 'move';
|
|
activeData = this.p2.active.map(function (pokemon) {
|
|
if (pokemon) return pokemon.getRequestData();
|
|
});
|
|
p2request = {active: activeData, side: this.p2.getData(), rqid: this.rqid};
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (this.p1 && this.p2) {
|
|
let inactiveSide = -1;
|
|
if (p1request && !p2request) {
|
|
inactiveSide = 0;
|
|
} else if (!p1request && p2request) {
|
|
inactiveSide = 1;
|
|
}
|
|
if (inactiveSide !== this.inactiveSide) {
|
|
this.send('inactiveside', inactiveSide);
|
|
this.inactiveSide = inactiveSide;
|
|
}
|
|
}
|
|
|
|
if (p1request) {
|
|
if (!this.supportCancel || !p2request) p1request.noCancel = true;
|
|
this.p1.emitRequest(p1request);
|
|
} else {
|
|
this.p1.decision = true;
|
|
this.p1.emitRequest({wait: true, side: this.p1.getData()});
|
|
}
|
|
|
|
if (p2request) {
|
|
if (!this.supportCancel || !p1request) p2request.noCancel = true;
|
|
this.p2.emitRequest(p2request);
|
|
} else {
|
|
this.p2.decision = true;
|
|
this.p2.emitRequest({wait: true, side: this.p2.getData()});
|
|
}
|
|
|
|
if (this.p2.decision && this.p1.decision) {
|
|
if (this.p2.decision === true && this.p1.decision === true) {
|
|
if (type !== 'move') {
|
|
// TODO: investigate this race condition; should be fixed
|
|
// properly later
|
|
return this.makeRequest('move');
|
|
}
|
|
this.add('html', '<div class="broadcast-red"><b>The battle crashed</b></div>');
|
|
this.win();
|
|
} else {
|
|
// some kind of weird race condition?
|
|
this.commitDecisions();
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
Battle.prototype.tie = function () {
|
|
this.win();
|
|
};
|
|
Battle.prototype.win = function (side) {
|
|
if (this.ended) {
|
|
return false;
|
|
}
|
|
if (side === 'p1' || side === 'p2') {
|
|
side = this[side];
|
|
} else if (side !== this.p1 && side !== this.p2) {
|
|
side = null;
|
|
}
|
|
this.winner = side ? side.name : '';
|
|
|
|
this.add('');
|
|
if (side) {
|
|
this.add('win', side.name);
|
|
} else {
|
|
this.add('tie');
|
|
}
|
|
this.ended = true;
|
|
this.active = false;
|
|
this.currentRequest = '';
|
|
this.currentRequestDetails = '';
|
|
return true;
|
|
};
|
|
Battle.prototype.switchIn = function (pokemon, pos) {
|
|
if (!pokemon || pokemon.isActive) return false;
|
|
if (!pos) pos = 0;
|
|
let side = pokemon.side;
|
|
if (pos >= side.active.length) {
|
|
throw new Error("Invalid switch position");
|
|
}
|
|
if (side.active[pos]) {
|
|
let oldActive = side.active[pos];
|
|
if (this.cancelMove(oldActive)) {
|
|
for (let i = 0; i < side.foe.active.length; i++) {
|
|
if (side.foe.active[i].isStale >= 2) {
|
|
oldActive.isStaleCon++;
|
|
oldActive.isStaleSource = 'drag';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (oldActive.switchCopyFlag === 'copyvolatile') {
|
|
delete oldActive.switchCopyFlag;
|
|
pokemon.copyVolatileFrom(oldActive);
|
|
}
|
|
}
|
|
pokemon.isActive = true;
|
|
this.runEvent('BeforeSwitchIn', pokemon);
|
|
if (side.active[pos]) {
|
|
let oldActive = side.active[pos];
|
|
oldActive.isActive = false;
|
|
oldActive.isStarted = false;
|
|
oldActive.usedItemThisTurn = false;
|
|
oldActive.position = pokemon.position;
|
|
pokemon.position = pos;
|
|
side.pokemon[pokemon.position] = pokemon;
|
|
side.pokemon[oldActive.position] = oldActive;
|
|
this.cancelMove(oldActive);
|
|
oldActive.clearVolatile();
|
|
}
|
|
side.active[pos] = pokemon;
|
|
pokemon.activeTurns = 0;
|
|
for (let m in pokemon.moveset) {
|
|
pokemon.moveset[m].used = false;
|
|
}
|
|
this.add('switch', pokemon, pokemon.getDetails);
|
|
pokemon.update();
|
|
this.insertQueue({pokemon: pokemon, choice: 'runSwitch'});
|
|
};
|
|
Battle.prototype.canSwitch = function (side) {
|
|
let canSwitchIn = [];
|
|
for (let i = side.active.length; i < side.pokemon.length; i++) {
|
|
let pokemon = side.pokemon[i];
|
|
if (!pokemon.fainted) {
|
|
canSwitchIn.push(pokemon);
|
|
}
|
|
}
|
|
return canSwitchIn.length;
|
|
};
|
|
Battle.prototype.getRandomSwitchable = function (side) {
|
|
let canSwitchIn = [];
|
|
for (let i = side.active.length; i < side.pokemon.length; i++) {
|
|
let pokemon = side.pokemon[i];
|
|
if (!pokemon.fainted) {
|
|
canSwitchIn.push(pokemon);
|
|
}
|
|
}
|
|
if (!canSwitchIn.length) {
|
|
return null;
|
|
}
|
|
return canSwitchIn[this.random(canSwitchIn.length)];
|
|
};
|
|
Battle.prototype.dragIn = function (side, pos) {
|
|
if (pos >= side.active.length) return false;
|
|
let pokemon = this.getRandomSwitchable(side);
|
|
if (!pos) pos = 0;
|
|
if (!pokemon || pokemon.isActive) return false;
|
|
this.runEvent('BeforeSwitchIn', pokemon);
|
|
if (side.active[pos]) {
|
|
let oldActive = side.active[pos];
|
|
if (!oldActive.hp) {
|
|
return false;
|
|
}
|
|
if (!this.runEvent('DragOut', oldActive)) {
|
|
return false;
|
|
}
|
|
this.runEvent('SwitchOut', oldActive);
|
|
this.singleEvent('End', this.getAbility(oldActive.ability), oldActive.abilityData, oldActive);
|
|
oldActive.isActive = false;
|
|
oldActive.isStarted = false;
|
|
oldActive.usedItemThisTurn = false;
|
|
oldActive.position = pokemon.position;
|
|
pokemon.position = pos;
|
|
side.pokemon[pokemon.position] = pokemon;
|
|
side.pokemon[oldActive.position] = oldActive;
|
|
if (this.cancelMove(oldActive)) {
|
|
for (let i = 0; i < side.foe.active.length; i++) {
|
|
if (side.foe.active[i].isStale >= 2) {
|
|
oldActive.isStaleCon++;
|
|
oldActive.isStaleSource = 'drag';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
oldActive.clearVolatile();
|
|
}
|
|
side.active[pos] = pokemon;
|
|
pokemon.isActive = true;
|
|
pokemon.activeTurns = 0;
|
|
if (this.gen === 2) pokemon.draggedIn = this.turn;
|
|
for (let m in pokemon.moveset) {
|
|
pokemon.moveset[m].used = false;
|
|
}
|
|
this.add('drag', pokemon, pokemon.getDetails);
|
|
pokemon.update();
|
|
if (this.gen >= 5) {
|
|
this.runEvent('SwitchIn', pokemon);
|
|
if (!pokemon.hp) return true;
|
|
pokemon.isStarted = true;
|
|
if (!pokemon.fainted) {
|
|
this.singleEvent('Start', pokemon.getAbility(), pokemon.abilityData, pokemon);
|
|
this.singleEvent('Start', pokemon.getItem(), pokemon.itemData, pokemon);
|
|
}
|
|
} else {
|
|
this.insertQueue({pokemon: pokemon, choice: 'runSwitch'});
|
|
}
|
|
return true;
|
|
};
|
|
Battle.prototype.swapPosition = function (pokemon, slot, attributes) {
|
|
if (slot >= pokemon.side.active.length) {
|
|
throw new Error("Invalid swap position");
|
|
}
|
|
let target = pokemon.side.active[slot];
|
|
if (slot !== 1 && (!target || target.fainted)) return false;
|
|
|
|
this.add('swap', pokemon, slot, attributes || '');
|
|
|
|
let side = pokemon.side;
|
|
side.pokemon[pokemon.position] = target;
|
|
side.pokemon[slot] = pokemon;
|
|
side.active[pokemon.position] = side.pokemon[pokemon.position];
|
|
side.active[slot] = side.pokemon[slot];
|
|
if (target) target.position = pokemon.position;
|
|
pokemon.position = slot;
|
|
return true;
|
|
};
|
|
Battle.prototype.faint = function (pokemon, source, effect) {
|
|
pokemon.faint(source, effect);
|
|
};
|
|
Battle.prototype.nextTurn = function () {
|
|
this.turn++;
|
|
let allStale = true;
|
|
let oneStale = false;
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
for (let j = 0; j < this.sides[i].active.length; j++) {
|
|
let pokemon = this.sides[i].active[j];
|
|
if (!pokemon) continue;
|
|
pokemon.moveThisTurn = '';
|
|
pokemon.usedItemThisTurn = false;
|
|
pokemon.newlySwitched = false;
|
|
pokemon.disabledMoves = {};
|
|
this.runEvent('DisableMove', pokemon);
|
|
if (!pokemon.ateBerry) pokemon.disableMove('belch');
|
|
if (pokemon.lastAttackedBy) {
|
|
if (pokemon.lastAttackedBy.pokemon.isActive) {
|
|
pokemon.lastAttackedBy.thisTurn = false;
|
|
} else {
|
|
pokemon.lastAttackedBy = null;
|
|
}
|
|
}
|
|
|
|
if (pokemon.fainted) continue;
|
|
if (pokemon.isStale < 2) {
|
|
if (pokemon.isStaleCon >= 2) {
|
|
if (pokemon.hp >= pokemon.isStaleHP - pokemon.maxhp / 100) {
|
|
pokemon.isStale++;
|
|
if (this.firstStaleWarned && pokemon.isStale < 2) {
|
|
switch (pokemon.isStaleSource) {
|
|
case 'struggle':
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(pokemon.name) + ' isn\'t losing HP from Struggle. If this continues, it will be classified as being in an endless loop.</div>');
|
|
break;
|
|
case 'drag':
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(pokemon.name) + ' isn\'t losing PP or HP from being forced to switch. If this continues, it will be classified as being in an endless loop.</div>');
|
|
break;
|
|
case 'switch':
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(pokemon.name) + ' isn\'t losing PP or HP from repeatedly switching. If this continues, it will be classified as being in an endless loop.</div>');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
pokemon.isStaleCon = 0;
|
|
pokemon.isStalePPTurns = 0;
|
|
pokemon.isStaleHP = pokemon.hp;
|
|
}
|
|
if (pokemon.isStalePPTurns >= 5) {
|
|
if (pokemon.hp >= pokemon.isStaleHP - pokemon.maxhp / 100) {
|
|
pokemon.isStale++;
|
|
pokemon.isStaleSource = 'ppstall';
|
|
if (this.firstStaleWarned && pokemon.isStale < 2) {
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(pokemon.name) + ' isn\'t losing PP or HP. If it keeps on not losing PP or HP, it will be classified as being in an endless loop.</div>');
|
|
}
|
|
}
|
|
pokemon.isStaleCon = 0;
|
|
pokemon.isStalePPTurns = 0;
|
|
pokemon.isStaleHP = pokemon.hp;
|
|
}
|
|
}
|
|
if (pokemon.getMoves().length === 0) {
|
|
pokemon.isStaleCon++;
|
|
pokemon.isStaleSource = 'struggle';
|
|
}
|
|
if (pokemon.isStale < 2) {
|
|
allStale = false;
|
|
} else if (pokemon.isStale && !pokemon.staleWarned) {
|
|
oneStale = pokemon;
|
|
}
|
|
if (!pokemon.isStalePPTurns) {
|
|
pokemon.isStaleHP = pokemon.hp;
|
|
if (pokemon.activeTurns) pokemon.isStaleCon = 0;
|
|
}
|
|
if (pokemon.activeTurns) {
|
|
pokemon.isStalePPTurns++;
|
|
}
|
|
pokemon.activeTurns++;
|
|
}
|
|
this.sides[i].faintedLastTurn = this.sides[i].faintedThisTurn;
|
|
this.sides[i].faintedThisTurn = false;
|
|
}
|
|
let banlistTable = this.getFormat().banlistTable;
|
|
if (banlistTable && 'Rule:endlessbattleclause' in banlistTable) {
|
|
if (oneStale) {
|
|
let activationWarning = '<br />If all active Pokémon go in an endless loop, Endless Battle Clause will activate.';
|
|
if (allStale) activationWarning = '';
|
|
let loopReason = '';
|
|
switch (oneStale.isStaleSource) {
|
|
case 'struggle':
|
|
loopReason = ": it isn't losing HP from Struggle";
|
|
break;
|
|
case 'drag':
|
|
loopReason = ": it isn't losing PP or HP from being forced to switch";
|
|
break;
|
|
case 'switch':
|
|
loopReason = ": it isn't losing PP or HP from repeatedly switching";
|
|
break;
|
|
case 'getleppa':
|
|
loopReason = ": it got a Leppa Berry it didn't start with";
|
|
break;
|
|
case 'useleppa':
|
|
loopReason = ": it used a Leppa Berry it didn't start with";
|
|
break;
|
|
case 'ppstall':
|
|
loopReason = ": it isn't losing PP or HP";
|
|
break;
|
|
case 'ppoverflow':
|
|
loopReason = ": its PP overflowed";
|
|
break;
|
|
}
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(oneStale.name) + ' is in an endless loop' + loopReason + '.' + activationWarning + '</div>');
|
|
oneStale.staleWarned = true;
|
|
this.firstStaleWarned = true;
|
|
}
|
|
if (allStale) {
|
|
this.add('message', "All active Pok\u00e9mon are in an endless loop. Endless Battle Clause activated!");
|
|
let leppaPokemon = null;
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
for (let j = 0; j < this.sides[i].pokemon.length; j++) {
|
|
let pokemon = this.sides[i].pokemon[j];
|
|
if (toId(pokemon.set.item) === 'leppaberry') {
|
|
if (leppaPokemon) {
|
|
leppaPokemon = null; // both sides have Leppa
|
|
this.add('-message', "Both sides started with a Leppa Berry.");
|
|
} else {
|
|
leppaPokemon = pokemon;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (leppaPokemon) {
|
|
this.add('-message', "" + leppaPokemon.side.name + "'s " + leppaPokemon.name + " started with a Leppa Berry and loses.");
|
|
this.win(leppaPokemon.side.foe);
|
|
return;
|
|
}
|
|
this.win();
|
|
return;
|
|
}
|
|
} else {
|
|
if (allStale && !this.staleWarned) {
|
|
this.staleWarned = true;
|
|
this.add('html', '<div class="broadcast-red">If this format had Endless Battle Clause, it would have activated.</div>');
|
|
} else if (oneStale) {
|
|
this.add('html', '<div class="broadcast-red">' + this.escapeHTML(oneStale.name) + ' is in an endless loop.</div>');
|
|
oneStale.staleWarned = true;
|
|
}
|
|
}
|
|
|
|
if (this.gameType === 'triples' && this.sides.map('pokemonLeft').count(1) === this.sides.length) {
|
|
// If both sides have one Pokemon left in triples and they are not adjacent, they are both moved to the center.
|
|
let center = false;
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
for (let j = 0; j < this.sides[i].active.length; j++) {
|
|
if (!this.sides[i].active[j] || this.sides[i].active[j].fainted) continue;
|
|
if (this.sides[i].active[j].position === 1) break;
|
|
this.swapPosition(this.sides[i].active[j], 1, '[silent]');
|
|
center = true;
|
|
break;
|
|
}
|
|
}
|
|
if (center) this.add('-center');
|
|
}
|
|
|
|
this.add('turn', this.turn);
|
|
|
|
this.makeRequest('move');
|
|
};
|
|
Battle.prototype.start = function () {
|
|
if (this.active) return;
|
|
|
|
if (!this.p1 || !this.p1.isActive || !this.p2 || !this.p2.isActive) {
|
|
// need two players to start
|
|
return;
|
|
}
|
|
|
|
this.p2.emitRequest({side: this.p2.getData()});
|
|
this.p1.emitRequest({side: this.p1.getData()});
|
|
|
|
if (this.started) {
|
|
this.makeRequest();
|
|
this.isActive = true;
|
|
this.activeTurns = 0;
|
|
return;
|
|
}
|
|
this.isActive = true;
|
|
this.activeTurns = 0;
|
|
this.started = true;
|
|
this.p2.foe = this.p1;
|
|
this.p1.foe = this.p2;
|
|
|
|
this.add('gametype', this.gameType);
|
|
this.add('gen', this.gen);
|
|
|
|
let format = this.getFormat();
|
|
Tools.mod(format.mod).getBanlistTable(format); // fill in format ruleset
|
|
|
|
this.add('tier', format.name);
|
|
if (this.rated) {
|
|
this.add('rated');
|
|
}
|
|
this.add('seed', Battle.logReplay.bind(this, this.startingSeed.join(',')));
|
|
if (format && format.ruleset) {
|
|
for (let i = 0; i < format.ruleset.length; i++) {
|
|
this.addPseudoWeather(format.ruleset[i]);
|
|
}
|
|
}
|
|
|
|
if (!this.p1.pokemon[0] || !this.p2.pokemon[0]) {
|
|
this.add('message', 'Battle not started: One of you has an empty team.');
|
|
return;
|
|
}
|
|
|
|
this.residualEvent('TeamPreview');
|
|
|
|
this.addQueue({choice: 'start'});
|
|
this.midTurn = true;
|
|
if (!this.currentRequest) this.go();
|
|
};
|
|
Battle.prototype.boost = function (boost, target, source, effect) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
if (!source) source = this.event.source;
|
|
if (!effect) effect = this.effect;
|
|
}
|
|
if (!target || !target.hp) return 0;
|
|
if (!target.isActive) return false;
|
|
effect = this.getEffect(effect);
|
|
boost = this.runEvent('Boost', target, source, effect, Object.clone(boost));
|
|
let success = false;
|
|
let boosted = false;
|
|
for (let i in boost) {
|
|
let currentBoost = {};
|
|
currentBoost[i] = boost[i];
|
|
if (boost[i] !== 0 && target.boostBy(currentBoost)) {
|
|
success = true;
|
|
let msg = '-boost';
|
|
if (boost[i] < 0) {
|
|
msg = '-unboost';
|
|
boost[i] = -boost[i];
|
|
}
|
|
switch (effect.id) {
|
|
case 'bellydrum':
|
|
this.add('-setboost', target, 'atk', target.boosts['atk'], '[from] move: Belly Drum');
|
|
break;
|
|
case 'bellydrum2':
|
|
this.add(msg, target, i, boost[i], '[silent]');
|
|
this.add('-hint', "In Gen 2, Belly Drum boosts by 2 when it fails.");
|
|
break;
|
|
case 'intimidate': case 'gooey':
|
|
this.add(msg, target, i, boost[i]);
|
|
break;
|
|
default:
|
|
if (effect.effectType === 'Move') {
|
|
this.add(msg, target, i, boost[i]);
|
|
} else {
|
|
if (effect.effectType === 'Ability' && !boosted) {
|
|
this.add('-activate', target, effect.fullname);
|
|
boosted = true;
|
|
}
|
|
this.add(msg, target, i, boost[i], '[from] ' + effect.fullname);
|
|
}
|
|
break;
|
|
}
|
|
this.runEvent('AfterEachBoost', target, source, effect, currentBoost);
|
|
}
|
|
}
|
|
this.runEvent('AfterBoost', target, source, effect, boost);
|
|
return success;
|
|
};
|
|
Battle.prototype.damage = function (damage, target, source, effect, instafaint) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
if (!source) source = this.event.source;
|
|
if (!effect) effect = this.effect;
|
|
}
|
|
if (!target || !target.hp) return 0;
|
|
if (!target.isActive) return false;
|
|
effect = this.getEffect(effect);
|
|
if (!(damage || damage === 0)) return damage;
|
|
if (damage !== 0) damage = this.clampIntRange(damage, 1);
|
|
|
|
if (effect.id !== 'struggle-recoil') { // Struggle recoil is not affected by effects
|
|
if (effect.effectType === 'Weather' && !target.runImmunity(effect.id)) {
|
|
this.debug('weather immunity');
|
|
return 0;
|
|
}
|
|
damage = this.runEvent('Damage', target, source, effect, damage);
|
|
if (!(damage || damage === 0)) {
|
|
this.debug('damage event failed');
|
|
return damage;
|
|
}
|
|
if (target.illusion && effect && effect.effectType === 'Move' && effect.id !== 'confused') {
|
|
this.debug('illusion cleared');
|
|
target.illusion = null;
|
|
this.add('replace', target, target.getDetails);
|
|
}
|
|
}
|
|
if (damage !== 0) damage = this.clampIntRange(damage, 1);
|
|
damage = target.damage(damage, source, effect);
|
|
if (source) source.lastDamage = damage;
|
|
let name = effect.fullname;
|
|
if (name === 'tox') name = 'psn';
|
|
switch (effect.id) {
|
|
case 'partiallytrapped':
|
|
this.add('-damage', target, target.getHealth, '[from] ' + this.effectData.sourceEffect.fullname, '[partiallytrapped]');
|
|
break;
|
|
case 'powder':
|
|
this.add('-damage', target, target.getHealth, '[silent]');
|
|
break;
|
|
case 'confused':
|
|
this.add('-damage', target, target.getHealth, '[from] confusion');
|
|
break;
|
|
default:
|
|
if (effect.effectType === 'Move') {
|
|
this.add('-damage', target, target.getHealth);
|
|
} else if (source && (source !== target || effect.effectType === 'Ability')) {
|
|
this.add('-damage', target, target.getHealth, '[from] ' + effect.fullname, '[of] ' + source);
|
|
} else {
|
|
this.add('-damage', target, target.getHealth, '[from] ' + name);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (effect.drain && source) {
|
|
this.heal(Math.ceil(damage * effect.drain[0] / effect.drain[1]), source, target, 'drain');
|
|
}
|
|
|
|
if (!effect.flags) effect.flags = {};
|
|
|
|
if (instafaint && !target.hp) {
|
|
this.debug('instafaint: ' + this.faintQueue.map('target').map('name'));
|
|
this.faintMessages(true);
|
|
} else {
|
|
damage = this.runEvent('AfterDamage', target, source, effect, damage);
|
|
}
|
|
|
|
return damage;
|
|
};
|
|
Battle.prototype.directDamage = function (damage, target, source, effect) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
if (!source) source = this.event.source;
|
|
if (!effect) effect = this.effect;
|
|
}
|
|
if (!target || !target.hp) return 0;
|
|
if (!damage) return 0;
|
|
damage = this.clampIntRange(damage, 1);
|
|
|
|
damage = target.damage(damage, source, effect);
|
|
switch (effect.id) {
|
|
case 'strugglerecoil':
|
|
this.add('-damage', target, target.getHealth, '[from] recoil');
|
|
break;
|
|
case 'confusion':
|
|
this.add('-damage', target, target.getHealth, '[from] confusion');
|
|
break;
|
|
default:
|
|
this.add('-damage', target, target.getHealth);
|
|
break;
|
|
}
|
|
if (target.fainted) this.faint(target);
|
|
return damage;
|
|
};
|
|
Battle.prototype.heal = function (damage, target, source, effect) {
|
|
if (this.event) {
|
|
if (!target) target = this.event.target;
|
|
if (!source) source = this.event.source;
|
|
if (!effect) effect = this.effect;
|
|
}
|
|
effect = this.getEffect(effect);
|
|
if (damage && damage <= 1) damage = 1;
|
|
damage = Math.floor(damage);
|
|
// for things like Liquid Ooze, the Heal event still happens when nothing is healed.
|
|
damage = this.runEvent('TryHeal', target, source, effect, damage);
|
|
if (!damage) return 0;
|
|
if (!target || !target.hp) return 0;
|
|
if (!target.isActive) return false;
|
|
if (target.hp >= target.maxhp) return 0;
|
|
damage = target.heal(damage, source, effect);
|
|
switch (effect.id) {
|
|
case 'leechseed':
|
|
case 'rest':
|
|
this.add('-heal', target, target.getHealth, '[silent]');
|
|
break;
|
|
case 'drain':
|
|
this.add('-heal', target, target.getHealth, '[from] drain', '[of] ' + source);
|
|
break;
|
|
case 'wish':
|
|
break;
|
|
default:
|
|
if (effect.effectType === 'Move') {
|
|
this.add('-heal', target, target.getHealth);
|
|
} else if (source && source !== target) {
|
|
this.add('-heal', target, target.getHealth, '[from] ' + effect.fullname, '[of] ' + source);
|
|
} else {
|
|
this.add('-heal', target, target.getHealth, '[from] ' + effect.fullname);
|
|
}
|
|
break;
|
|
}
|
|
this.runEvent('Heal', target, source, effect, damage);
|
|
return damage;
|
|
};
|
|
Battle.prototype.chain = function (previousMod, nextMod) {
|
|
// previousMod or nextMod can be either a number or an array [numerator, denominator]
|
|
if (previousMod.length) {
|
|
previousMod = Math.floor(previousMod[0] * 4096 / previousMod[1]);
|
|
} else {
|
|
previousMod = Math.floor(previousMod * 4096);
|
|
}
|
|
|
|
if (nextMod.length) {
|
|
nextMod = Math.floor(nextMod[0] * 4096 / nextMod[1]);
|
|
} else {
|
|
nextMod = Math.floor(nextMod * 4096);
|
|
}
|
|
return ((previousMod * nextMod + 2048) >> 12) / 4096; // M'' = ((M * M') + 0x800) >> 12
|
|
};
|
|
Battle.prototype.chainModify = function (numerator, denominator) {
|
|
let previousMod = Math.floor(this.event.modifier * 4096);
|
|
|
|
if (numerator.length) {
|
|
denominator = numerator[1];
|
|
numerator = numerator[0];
|
|
}
|
|
let nextMod = 0;
|
|
if (this.event.ceilModifier) {
|
|
nextMod = Math.ceil(numerator * 4096 / (denominator || 1));
|
|
} else {
|
|
nextMod = Math.floor(numerator * 4096 / (denominator || 1));
|
|
}
|
|
|
|
this.event.modifier = ((previousMod * nextMod + 2048) >> 12) / 4096;
|
|
};
|
|
Battle.prototype.modify = function (value, numerator, denominator) {
|
|
// You can also use:
|
|
// modify(value, [numerator, denominator])
|
|
// modify(value, fraction) - assuming you trust JavaScript's floating-point handler
|
|
if (!denominator) denominator = 1;
|
|
if (numerator && numerator.length) {
|
|
denominator = numerator[1];
|
|
numerator = numerator[0];
|
|
}
|
|
let modifier = Math.floor(numerator * 4096 / denominator);
|
|
return Math.floor((value * modifier + 2048 - 1) / 4096);
|
|
};
|
|
Battle.prototype.getCategory = function (move) {
|
|
move = this.getMove(move);
|
|
return move.category || 'Physical';
|
|
};
|
|
Battle.prototype.getDamage = function (pokemon, target, move, suppressMessages) {
|
|
if (typeof move === 'string') move = this.getMove(move);
|
|
|
|
if (typeof move === 'number') move = {
|
|
basePower: move,
|
|
type: '???',
|
|
category: 'Physical',
|
|
flags: {}
|
|
};
|
|
|
|
if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) {
|
|
if (!target.runImmunity(move.type, !suppressMessages)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (move.ohko) {
|
|
return target.maxhp;
|
|
}
|
|
|
|
if (move.damageCallback) {
|
|
return move.damageCallback.call(this, pokemon, target);
|
|
}
|
|
if (move.damage === 'level') {
|
|
return pokemon.level;
|
|
}
|
|
if (move.damage) {
|
|
return move.damage;
|
|
}
|
|
|
|
if (!move) {
|
|
move = {};
|
|
}
|
|
if (!move.type) move.type = '???';
|
|
let type = move.type;
|
|
// '???' is typeless damage: used for Struggle and Confusion etc
|
|
let category = this.getCategory(move);
|
|
let defensiveCategory = move.defensiveCategory || category;
|
|
|
|
let basePower = move.basePower;
|
|
if (move.basePowerCallback) {
|
|
basePower = move.basePowerCallback.call(this, pokemon, target, move);
|
|
}
|
|
if (!basePower) {
|
|
if (basePower === 0) return; // returning undefined means not dealing damage
|
|
return basePower;
|
|
}
|
|
basePower = this.clampIntRange(basePower, 1);
|
|
|
|
let critMult;
|
|
if (this.gen <= 5) {
|
|
move.critRatio = this.clampIntRange(move.critRatio, 0, 5);
|
|
critMult = [0, 16, 8, 4, 3, 2];
|
|
} else {
|
|
move.critRatio = this.clampIntRange(move.critRatio, 0, 4);
|
|
critMult = [0, 16, 8, 2, 1];
|
|
}
|
|
|
|
move.crit = move.willCrit || false;
|
|
if (move.willCrit === undefined) {
|
|
if (move.critRatio) {
|
|
move.crit = (this.random(critMult[move.critRatio]) === 0);
|
|
}
|
|
}
|
|
|
|
if (move.crit) {
|
|
move.crit = this.runEvent('CriticalHit', target, null, move);
|
|
}
|
|
|
|
// happens after crit calculation
|
|
basePower = this.runEvent('BasePower', pokemon, target, move, basePower, true);
|
|
|
|
if (!basePower) return 0;
|
|
basePower = this.clampIntRange(basePower, 1);
|
|
|
|
let level = pokemon.level;
|
|
|
|
let attacker = pokemon;
|
|
let defender = target;
|
|
let attackStat = category === 'Physical' ? 'atk' : 'spa';
|
|
let defenseStat = defensiveCategory === 'Physical' ? 'def' : 'spd';
|
|
let statTable = {atk:'Atk', def:'Def', spa:'SpA', spd:'SpD', spe:'Spe'};
|
|
let attack;
|
|
let defense;
|
|
|
|
let atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat];
|
|
let defBoosts = move.useSourceDefensive ? attacker.boosts[defenseStat] : defender.boosts[defenseStat];
|
|
|
|
let ignoreNegativeOffensive = !!move.ignoreNegativeOffensive;
|
|
let ignorePositiveDefensive = !!move.ignorePositiveDefensive;
|
|
|
|
if (move.crit) {
|
|
ignoreNegativeOffensive = true;
|
|
ignorePositiveDefensive = true;
|
|
}
|
|
let ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0));
|
|
let ignoreDefensive = !!(move.ignoreDefensive || (ignorePositiveDefensive && defBoosts > 0));
|
|
|
|
if (ignoreOffensive) {
|
|
this.debug('Negating (sp)atk boost/penalty.');
|
|
atkBoosts = 0;
|
|
}
|
|
if (ignoreDefensive) {
|
|
this.debug('Negating (sp)def boost/penalty.');
|
|
defBoosts = 0;
|
|
}
|
|
|
|
if (move.useTargetOffensive) {
|
|
attack = defender.calculateStat(attackStat, atkBoosts);
|
|
} else {
|
|
attack = attacker.calculateStat(attackStat, atkBoosts);
|
|
}
|
|
|
|
if (move.useSourceDefensive) {
|
|
defense = attacker.calculateStat(defenseStat, defBoosts);
|
|
} else {
|
|
defense = defender.calculateStat(defenseStat, defBoosts);
|
|
}
|
|
|
|
// Apply Stat Modifiers
|
|
attack = this.runEvent('Modify' + statTable[attackStat], attacker, defender, move, attack);
|
|
defense = this.runEvent('Modify' + statTable[defenseStat], defender, attacker, move, defense);
|
|
|
|
//int(int(int(2 * L / 5 + 2) * A * P / D) / 50);
|
|
let baseDamage = Math.floor(Math.floor(Math.floor(2 * level / 5 + 2) * basePower * attack / defense) / 50) + 2;
|
|
|
|
// multi-target modifier (doubles only)
|
|
if (move.spreadHit) {
|
|
let spreadModifier = move.spreadModifier || 0.75;
|
|
this.debug('Spread modifier: ' + spreadModifier);
|
|
baseDamage = this.modify(baseDamage, spreadModifier);
|
|
}
|
|
|
|
// weather modifier
|
|
baseDamage = this.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
// crit
|
|
if (move.crit) {
|
|
baseDamage = this.modify(baseDamage, move.critModifier || (this.gen >= 6 ? 1.5 : 2));
|
|
}
|
|
|
|
// this is not a modifier
|
|
baseDamage = this.randomizer(baseDamage);
|
|
|
|
// STAB
|
|
if (move.hasSTAB || type !== '???' && pokemon.hasType(type)) {
|
|
// The "???" type never gets STAB
|
|
// Not even if you Roost in Gen 4 and somehow manage to use
|
|
// Struggle in the same turn.
|
|
// (On second thought, it might be easier to get a Missingno.)
|
|
baseDamage = this.modify(baseDamage, move.stab || 1.5);
|
|
}
|
|
// types
|
|
move.typeMod = target.runEffectiveness(move);
|
|
|
|
move.typeMod = this.clampIntRange(move.typeMod, -6, 6);
|
|
if (move.typeMod > 0) {
|
|
if (!suppressMessages) this.add('-supereffective', target);
|
|
|
|
for (let i = 0; i < move.typeMod; i++) {
|
|
baseDamage *= 2;
|
|
}
|
|
}
|
|
if (move.typeMod < 0) {
|
|
if (!suppressMessages) this.add('-resisted', target);
|
|
|
|
for (let i = 0; i > move.typeMod; i--) {
|
|
baseDamage = Math.floor(baseDamage / 2);
|
|
}
|
|
}
|
|
|
|
if (move.crit && !suppressMessages) this.add('-crit', target);
|
|
|
|
if (pokemon.status === 'brn' && basePower && move.category === 'Physical' && !pokemon.hasAbility('guts')) {
|
|
if (this.gen < 6 || move.id !== 'facade') {
|
|
baseDamage = this.modify(baseDamage, 0.5);
|
|
}
|
|
}
|
|
|
|
// Generation 5 sets damage to 1 before the final damage modifiers only
|
|
if (this.gen === 5 && basePower && !Math.floor(baseDamage)) {
|
|
baseDamage = 1;
|
|
}
|
|
|
|
// Final modifier. Modifiers that modify damage after min damage check, such as Life Orb.
|
|
baseDamage = this.runEvent('ModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
if (this.gen !== 5 && basePower && !Math.floor(baseDamage)) {
|
|
return 1;
|
|
}
|
|
|
|
return Math.floor(baseDamage);
|
|
};
|
|
Battle.prototype.randomizer = function (baseDamage) {
|
|
return Math.floor(baseDamage * (100 - this.random(16)) / 100);
|
|
};
|
|
/**
|
|
* Returns whether a proposed target for a move is valid.
|
|
*/
|
|
Battle.prototype.validTargetLoc = function (targetLoc, source, targetType) {
|
|
let numSlots = source.side.active.length;
|
|
if (!Math.abs(targetLoc) && Math.abs(targetLoc) > numSlots) return false;
|
|
|
|
let sourceLoc = -(source.position + 1);
|
|
let isFoe = (targetLoc > 0);
|
|
let isAdjacent = (isFoe ? Math.abs(-(numSlots + 1 - targetLoc) - sourceLoc) <= 1 : Math.abs(targetLoc - sourceLoc) === 1);
|
|
let isSelf = (sourceLoc === targetLoc);
|
|
|
|
switch (targetType) {
|
|
case 'randomNormal':
|
|
case 'normal':
|
|
return isAdjacent;
|
|
case 'adjacentAlly':
|
|
return isAdjacent && !isFoe;
|
|
case 'adjacentAllyOrSelf':
|
|
return isAdjacent && !isFoe || isSelf;
|
|
case 'adjacentFoe':
|
|
return isAdjacent && isFoe;
|
|
case 'any':
|
|
return !isSelf;
|
|
}
|
|
return false;
|
|
};
|
|
Battle.prototype.getTargetLoc = function (target, source) {
|
|
if (target.side === source.side) {
|
|
return -(target.position + 1);
|
|
} else {
|
|
return target.position + 1;
|
|
}
|
|
};
|
|
Battle.prototype.validTarget = function (target, source, targetType) {
|
|
return this.validTargetLoc(this.getTargetLoc(target, source), source, targetType);
|
|
};
|
|
Battle.prototype.getTarget = function (decision) {
|
|
let move = this.getMove(decision.move);
|
|
let target;
|
|
if ((move.target !== 'randomNormal') &&
|
|
this.validTargetLoc(decision.targetLoc, decision.pokemon, move.target)) {
|
|
if (decision.targetLoc > 0) {
|
|
target = decision.pokemon.side.foe.active[decision.targetLoc - 1];
|
|
} else {
|
|
target = decision.pokemon.side.active[-decision.targetLoc - 1];
|
|
}
|
|
if (target) {
|
|
if (!target.fainted) {
|
|
// target exists and is not fainted
|
|
return target;
|
|
} else if (target.side === decision.pokemon.side) {
|
|
// fainted allied targets don't retarget
|
|
return false;
|
|
}
|
|
}
|
|
// chosen target not valid, retarget randomly with resolveTarget
|
|
}
|
|
if (!decision.targetPosition || !decision.targetSide) {
|
|
target = this.resolveTarget(decision.pokemon, decision.move);
|
|
decision.targetSide = target.side;
|
|
decision.targetPosition = target.position;
|
|
}
|
|
return decision.targetSide.active[decision.targetPosition];
|
|
};
|
|
Battle.prototype.resolveTarget = function (pokemon, move) {
|
|
// A move was used without a chosen target
|
|
|
|
// For instance: Metronome chooses Ice Beam. Since the user didn't
|
|
// choose a target when choosing Metronome, Ice Beam's target must
|
|
// be chosen randomly.
|
|
|
|
// The target is chosen randomly from possible targets, EXCEPT that
|
|
// moves that can target either allies or foes will only target foes
|
|
// when used without an explicit target.
|
|
|
|
move = this.getMove(move);
|
|
if (move.target === 'adjacentAlly') {
|
|
let adjacentAllies = [pokemon.side.active[pokemon.position - 1], pokemon.side.active[pokemon.position + 1]].filter(function (active) {
|
|
return active && !active.fainted;
|
|
});
|
|
if (adjacentAllies.length) return adjacentAllies[Math.floor(Math.random() * adjacentAllies.length)];
|
|
return pokemon;
|
|
}
|
|
if (move.target === 'self' || move.target === 'all' || move.target === 'allySide' || move.target === 'allyTeam' || move.target === 'adjacentAllyOrSelf') {
|
|
return pokemon;
|
|
}
|
|
if (pokemon.side.active.length > 2) {
|
|
if (move.target === 'adjacentFoe' || move.target === 'normal' || move.target === 'randomNormal') {
|
|
let foeActives = pokemon.side.foe.active;
|
|
let frontPosition = foeActives.length - 1 - pokemon.position;
|
|
let adjacentFoes = foeActives.slice(frontPosition < 1 ? 0 : frontPosition - 1, frontPosition + 2).filter(function (active) {
|
|
return active && !active.fainted;
|
|
});
|
|
if (adjacentFoes.length) return adjacentFoes[Math.floor(Math.random() * adjacentFoes.length)];
|
|
// no valid target at all, return a foe for any possible redirection
|
|
}
|
|
}
|
|
return pokemon.side.foe.randomActive() || pokemon.side.foe.active[0];
|
|
};
|
|
Battle.prototype.checkFainted = function () {
|
|
function check(a) {
|
|
if (!a) return;
|
|
if (a.fainted) {
|
|
a.status = 'fnt';
|
|
a.switchFlag = true;
|
|
}
|
|
}
|
|
|
|
this.p1.active.forEach(check);
|
|
this.p2.active.forEach(check);
|
|
};
|
|
Battle.prototype.faintMessages = function (lastFirst) {
|
|
if (this.ended) return;
|
|
if (!this.faintQueue.length) return false;
|
|
if (lastFirst) {
|
|
this.faintQueue.unshift(this.faintQueue.pop());
|
|
}
|
|
let faintData;
|
|
while (this.faintQueue.length) {
|
|
faintData = this.faintQueue.shift();
|
|
if (!faintData.target.fainted) {
|
|
this.add('faint', faintData.target);
|
|
this.runEvent('Faint', faintData.target, faintData.source, faintData.effect);
|
|
this.singleEvent('End', this.getAbility(faintData.target.ability), faintData.target.abilityData, faintData.target);
|
|
faintData.target.fainted = true;
|
|
faintData.target.isActive = false;
|
|
faintData.target.isStarted = false;
|
|
faintData.target.side.pokemonLeft--;
|
|
faintData.target.side.faintedThisTurn = true;
|
|
}
|
|
}
|
|
|
|
if (this.gen <= 1) {
|
|
// in gen 1, fainting skips the rest of the turn, including residuals
|
|
this.queue = [];
|
|
} else if (this.gen <= 3 && this.gameType === 'singles') {
|
|
// in gen 3 or earlier, fainting in singles skips to residuals
|
|
for (let i = 0; i < this.p1.active.length; i++) {
|
|
this.cancelMove(this.p1.active[i]);
|
|
}
|
|
for (let i = 0; i < this.p2.active.length; i++) {
|
|
this.cancelMove(this.p2.active[i]);
|
|
}
|
|
}
|
|
|
|
if (!this.p1.pokemonLeft && !this.p2.pokemonLeft) {
|
|
this.win(faintData && faintData.target.side);
|
|
return true;
|
|
}
|
|
if (!this.p1.pokemonLeft) {
|
|
this.win(this.p2);
|
|
return true;
|
|
}
|
|
if (!this.p2.pokemonLeft) {
|
|
this.win(this.p1);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
Battle.prototype.resolvePriority = function (decision) {
|
|
if (decision) {
|
|
if (!decision.side && decision.pokemon) decision.side = decision.pokemon.side;
|
|
if (!decision.choice && decision.move) decision.choice = 'move';
|
|
if (!decision.priority && decision.priority !== 0) {
|
|
let priorities = {
|
|
'beforeTurn': 100,
|
|
'beforeTurnMove': 99,
|
|
'switch': 7,
|
|
'runSwitch': 7.1,
|
|
'instaswitch': 101,
|
|
'megaEvo': 6.9,
|
|
'residual': -100,
|
|
'team': 102,
|
|
'start': 101
|
|
};
|
|
if (decision.choice in priorities) {
|
|
decision.priority = priorities[decision.choice];
|
|
}
|
|
}
|
|
if (decision.choice === 'move') {
|
|
if (this.getMove(decision.move).beforeTurnCallback) {
|
|
this.addQueue({choice: 'beforeTurnMove', pokemon: decision.pokemon, move: decision.move, targetLoc: decision.targetLoc});
|
|
}
|
|
} else if (decision.choice === 'switch' || decision.choice === 'instaswitch') {
|
|
if (decision.pokemon.switchFlag && decision.pokemon.switchFlag !== true) {
|
|
decision.pokemon.switchCopyFlag = decision.pokemon.switchFlag;
|
|
}
|
|
decision.pokemon.switchFlag = false;
|
|
if (!decision.speed && decision.pokemon && decision.pokemon.isActive) decision.speed = decision.pokemon.speed;
|
|
}
|
|
if (decision.move) {
|
|
let target;
|
|
|
|
if (!decision.targetPosition) {
|
|
target = this.resolveTarget(decision.pokemon, decision.move);
|
|
decision.targetSide = target.side;
|
|
decision.targetPosition = target.position;
|
|
}
|
|
|
|
decision.move = this.getMoveCopy(decision.move);
|
|
if (!decision.priority) {
|
|
let priority = decision.move.priority;
|
|
priority = this.runEvent('ModifyPriority', decision.pokemon, target, decision.move, priority);
|
|
decision.priority = priority;
|
|
// In Gen 6, Quick Guard blocks moves with artificially enhanced priority.
|
|
if (this.gen > 5) decision.move.priority = priority;
|
|
}
|
|
}
|
|
if (!decision.pokemon && !decision.speed) decision.speed = 1;
|
|
if (!decision.speed && (decision.choice === 'switch' || decision.choice === 'instaswitch') && decision.target) decision.speed = decision.target.speed;
|
|
if (!decision.speed) decision.speed = decision.pokemon.speed;
|
|
}
|
|
};
|
|
Battle.prototype.addQueue = function (decision) {
|
|
if (Array.isArray(decision)) {
|
|
for (let i = 0; i < decision.length; i++) {
|
|
this.addQueue(decision[i]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.resolvePriority(decision);
|
|
this.queue.push(decision);
|
|
};
|
|
Battle.prototype.sortQueue = function () {
|
|
this.queue.sort(Battle.comparePriority);
|
|
};
|
|
Battle.prototype.insertQueue = function (decision) {
|
|
if (Array.isArray(decision)) {
|
|
for (let i = 0; i < decision.length; i++) {
|
|
this.insertQueue(decision[i]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.resolvePriority(decision);
|
|
for (let i = 0; i <= this.queue.length; i++) {
|
|
if (i === this.queue.length) {
|
|
this.queue.push(decision);
|
|
break;
|
|
} else if (Battle.comparePriority(decision, this.queue[i]) < 0) {
|
|
this.queue.splice(i, 0, decision);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
Battle.prototype.prioritizeQueue = function (decision, source, sourceEffect) {
|
|
if (this.event) {
|
|
if (!source) source = this.event.source;
|
|
if (!sourceEffect) sourceEffect = this.effect;
|
|
}
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if (this.queue[i] === decision) {
|
|
this.queue.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
decision.sourceEffect = sourceEffect;
|
|
this.queue.unshift(decision);
|
|
};
|
|
Battle.prototype.willAct = function () {
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if (this.queue[i].choice === 'move' || this.queue[i].choice === 'switch' || this.queue[i].choice === 'instaswitch' || this.queue[i].choice === 'shift') {
|
|
return this.queue[i];
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
Battle.prototype.willMove = function (pokemon) {
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if (this.queue[i].choice === 'move' && this.queue[i].pokemon === pokemon) {
|
|
return this.queue[i];
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
Battle.prototype.cancelDecision = function (pokemon) {
|
|
let success = false;
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if (this.queue[i].pokemon === pokemon) {
|
|
this.queue.splice(i, 1);
|
|
i--;
|
|
success = true;
|
|
}
|
|
}
|
|
return success;
|
|
};
|
|
Battle.prototype.cancelMove = function (pokemon) {
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if (this.queue[i].choice === 'move' && this.queue[i].pokemon === pokemon) {
|
|
this.queue.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
Battle.prototype.willSwitch = function (pokemon) {
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
if ((this.queue[i].choice === 'switch' || this.queue[i].choice === 'instaswitch') && this.queue[i].pokemon === pokemon) {
|
|
return this.queue[i];
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
Battle.prototype.runDecision = function (decision) {
|
|
// returns whether or not we ended in a callback
|
|
switch (decision.choice) {
|
|
case 'start': {
|
|
// I GIVE UP, WILL WRESTLE WITH EVENT SYSTEM LATER
|
|
let format = this.getFormat();
|
|
|
|
if (format.onBegin) format.onBegin.call(this);
|
|
|
|
if (format.teamLength && format.teamLength.battle) {
|
|
// Trim the team: not all of the Pokémon brought to Preview will battle.
|
|
this.p1.pokemon = this.p1.pokemon.slice(0, format.teamLength.battle);
|
|
this.p1.pokemonLeft = this.p1.pokemon.length;
|
|
this.p2.pokemon = this.p2.pokemon.slice(0, format.teamLength.battle);
|
|
this.p2.pokemonLeft = this.p2.pokemon.length;
|
|
}
|
|
|
|
this.add('start');
|
|
for (let pos = 0; pos < this.p1.active.length; pos++) {
|
|
this.switchIn(this.p1.pokemon[pos], pos);
|
|
}
|
|
for (let pos = 0; pos < this.p2.active.length; pos++) {
|
|
this.switchIn(this.p2.pokemon[pos], pos);
|
|
}
|
|
for (let pos = 0; pos < this.p1.pokemon.length; pos++) {
|
|
let pokemon = this.p1.pokemon[pos];
|
|
this.singleEvent('Start', this.getEffect(pokemon.species), pokemon.speciesData, pokemon);
|
|
}
|
|
for (let pos = 0; pos < this.p2.pokemon.length; pos++) {
|
|
let pokemon = this.p2.pokemon[pos];
|
|
this.singleEvent('Start', this.getEffect(pokemon.species), pokemon.speciesData, pokemon);
|
|
}
|
|
this.midTurn = true;
|
|
break;
|
|
}
|
|
|
|
case 'move':
|
|
if (!decision.pokemon.isActive) return false;
|
|
if (decision.pokemon.fainted) return false;
|
|
this.runMove(decision.move, decision.pokemon, this.getTarget(decision), decision.sourceEffect);
|
|
break;
|
|
case 'megaEvo':
|
|
if (decision.pokemon.canMegaEvo) this.runMegaEvo(decision.pokemon);
|
|
break;
|
|
case 'beforeTurnMove': {
|
|
if (!decision.pokemon.isActive) return false;
|
|
if (decision.pokemon.fainted) return false;
|
|
this.debug('before turn callback: ' + decision.move.id);
|
|
let target = this.getTarget(decision);
|
|
if (!target) return false;
|
|
decision.move.beforeTurnCallback.call(this, decision.pokemon, target);
|
|
break;
|
|
}
|
|
|
|
case 'event':
|
|
this.runEvent(decision.event, decision.pokemon);
|
|
break;
|
|
case 'team': {
|
|
let len = decision.side.pokemon.length;
|
|
let newPokemon = [null, null, null, null, null, null].slice(0, len);
|
|
for (let j = 0; j < len; j++) {
|
|
let i = decision.team[j];
|
|
newPokemon[j] = decision.side.pokemon[i];
|
|
newPokemon[j].position = j;
|
|
}
|
|
decision.side.pokemon = newPokemon;
|
|
|
|
// we return here because the update event would crash since there are no active pokemon yet
|
|
return;
|
|
}
|
|
|
|
case 'pass':
|
|
if (!decision.priority || decision.priority <= 101) return;
|
|
if (decision.pokemon) {
|
|
decision.pokemon.switchFlag = false;
|
|
}
|
|
break;
|
|
case 'instaswitch':
|
|
case 'switch':
|
|
if (decision.pokemon) {
|
|
decision.pokemon.beingCalledBack = true;
|
|
let lastMove = this.getMove(decision.pokemon.lastMove);
|
|
if (lastMove.selfSwitch !== 'copyvolatile') {
|
|
this.runEvent('BeforeSwitchOut', decision.pokemon);
|
|
if (this.gen >= 5) {
|
|
this.eachEvent('Update');
|
|
}
|
|
}
|
|
if (!this.runEvent('SwitchOut', decision.pokemon)) {
|
|
// Warning: DO NOT interrupt a switch-out
|
|
// if you just want to trap a pokemon.
|
|
// To trap a pokemon and prevent it from switching out,
|
|
// (e.g. Mean Look, Magnet Pull) use the 'trapped' flag
|
|
// instead.
|
|
|
|
// Note: Nothing in BW or earlier interrupts
|
|
// a switch-out.
|
|
break;
|
|
}
|
|
this.singleEvent('End', this.getAbility(decision.pokemon.ability), decision.pokemon.abilityData, decision.pokemon);
|
|
}
|
|
if (decision.pokemon && !decision.pokemon.hp && !decision.pokemon.fainted) {
|
|
// a pokemon fainted from Pursuit before it could switch
|
|
if (this.gen <= 4) {
|
|
// in gen 2-4, the switch still happens
|
|
decision.priority = -101;
|
|
this.queue.unshift(decision);
|
|
this.debug('Pursuit target fainted');
|
|
break;
|
|
}
|
|
// in gen 5+, the switch is cancelled
|
|
this.debug('A Pokemon can\'t switch between when it runs out of HP and when it faints');
|
|
break;
|
|
}
|
|
if (decision.target.isActive) {
|
|
this.debug('Switch target is already active');
|
|
break;
|
|
}
|
|
if (decision.choice === 'switch' && decision.pokemon.activeTurns === 1) {
|
|
let foeActive = decision.pokemon.side.foe.active;
|
|
for (let i = 0; i < foeActive.length; i++) {
|
|
if (foeActive[i].isStale >= 2) {
|
|
decision.pokemon.isStaleCon++;
|
|
decision.pokemon.isStaleSource = 'switch';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.switchIn(decision.target, decision.pokemon.position);
|
|
break;
|
|
case 'runSwitch':
|
|
this.runEvent('SwitchIn', decision.pokemon);
|
|
if (this.gen <= 2 && !decision.pokemon.side.faintedThisTurn && decision.pokemon.draggedIn !== this.turn) this.runEvent('AfterSwitchInSelf', decision.pokemon);
|
|
if (!decision.pokemon.hp) break;
|
|
decision.pokemon.isStarted = true;
|
|
if (!decision.pokemon.fainted) {
|
|
this.singleEvent('Start', decision.pokemon.getAbility(), decision.pokemon.abilityData, decision.pokemon);
|
|
this.singleEvent('Start', decision.pokemon.getItem(), decision.pokemon.itemData, decision.pokemon);
|
|
}
|
|
delete decision.pokemon.draggedIn;
|
|
break;
|
|
case 'shift': {
|
|
if (!decision.pokemon.isActive) return false;
|
|
if (decision.pokemon.fainted) return false;
|
|
decision.pokemon.activeTurns--;
|
|
this.swapPosition(decision.pokemon, 1);
|
|
let foeActive = decision.pokemon.side.foe.active;
|
|
for (let i = 0; i < foeActive.length; i++) {
|
|
if (foeActive[i].isStale >= 2) {
|
|
decision.pokemon.isStaleCon++;
|
|
decision.pokemon.isStaleSource = 'switch';
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'beforeTurn':
|
|
this.eachEvent('BeforeTurn');
|
|
break;
|
|
case 'residual':
|
|
this.add('');
|
|
this.clearActiveMove(true);
|
|
this.residualEvent('Residual');
|
|
break;
|
|
}
|
|
|
|
// phazing (Roar, etc)
|
|
|
|
let self = this;
|
|
function checkForceSwitchFlag(a) {
|
|
if (!a) return false;
|
|
if (a.hp && a.forceSwitchFlag) {
|
|
self.dragIn(a.side, a.position);
|
|
}
|
|
delete a.forceSwitchFlag;
|
|
}
|
|
this.p1.active.forEach(checkForceSwitchFlag);
|
|
this.p2.active.forEach(checkForceSwitchFlag);
|
|
|
|
this.clearActiveMove();
|
|
|
|
// fainting
|
|
|
|
this.faintMessages();
|
|
if (this.ended) return true;
|
|
|
|
// switching (fainted pokemon, U-turn, Baton Pass, etc)
|
|
|
|
if (!this.queue.length || (this.gen <= 3 && this.queue[0].choice in {move:1, residual:1})) {
|
|
// in gen 3 or earlier, switching in fainted pokemon is done after
|
|
// every move, rather than only at the end of the turn.
|
|
this.checkFainted();
|
|
} else if (decision.choice === 'pass') {
|
|
this.eachEvent('Update');
|
|
return false;
|
|
}
|
|
|
|
function hasSwitchFlag(a) { return a ? a.switchFlag : false; }
|
|
function removeSwitchFlag(a) { if (a) a.switchFlag = false; }
|
|
let p1switch = this.p1.active.any(hasSwitchFlag);
|
|
let p2switch = this.p2.active.any(hasSwitchFlag);
|
|
|
|
if (p1switch && !this.canSwitch(this.p1)) {
|
|
this.p1.active.forEach(removeSwitchFlag);
|
|
p1switch = false;
|
|
}
|
|
if (p2switch && !this.canSwitch(this.p2)) {
|
|
this.p2.active.forEach(removeSwitchFlag);
|
|
p2switch = false;
|
|
}
|
|
|
|
if (p1switch || p2switch) {
|
|
if (this.gen >= 5) {
|
|
this.eachEvent('Update');
|
|
}
|
|
this.makeRequest('switch');
|
|
return true;
|
|
}
|
|
|
|
this.eachEvent('Update');
|
|
|
|
return false;
|
|
};
|
|
Battle.prototype.go = function () {
|
|
this.add('');
|
|
if (this.currentRequest) {
|
|
this.currentRequest = '';
|
|
this.currentRequestDetails = '';
|
|
}
|
|
|
|
if (!this.midTurn) {
|
|
this.queue.push({choice: 'residual', priority: -100});
|
|
this.queue.unshift({choice: 'beforeTurn', priority: 100});
|
|
this.midTurn = true;
|
|
}
|
|
|
|
while (this.queue.length) {
|
|
let decision = this.queue.shift();
|
|
|
|
this.runDecision(decision);
|
|
|
|
if (this.currentRequest) {
|
|
return;
|
|
}
|
|
|
|
if (this.ended) return;
|
|
}
|
|
|
|
this.nextTurn();
|
|
this.midTurn = false;
|
|
this.queue = [];
|
|
};
|
|
/**
|
|
* Changes a pokemon's decision, and inserts its new decision
|
|
* in priority order.
|
|
*
|
|
* You'd normally want the OverrideDecision event (which doesn't
|
|
* change priority order).
|
|
*/
|
|
Battle.prototype.changeDecision = function (pokemon, decision) {
|
|
this.cancelDecision(pokemon);
|
|
if (!decision.pokemon) decision.pokemon = pokemon;
|
|
this.insertQueue(decision);
|
|
};
|
|
/**
|
|
* Takes a choice string passed from the client. Starts the next
|
|
* turn if all required choices have been made.
|
|
*/
|
|
Battle.prototype.choose = function (sideid, choice, rqid) {
|
|
let side = null;
|
|
if (sideid === 'p1' || sideid === 'p2') side = this[sideid];
|
|
// This condition should be impossible because the sideid comes
|
|
// from our forked process and if the player id were invalid, we would
|
|
// not have even got to this function.
|
|
if (!side) return; // wtf
|
|
|
|
// This condition can occur if the client sends a decision at the
|
|
// wrong time.
|
|
if (!side.currentRequest) return;
|
|
|
|
// Make sure the decision is for the right request.
|
|
if ((rqid !== undefined) && (parseInt(rqid, 10) !== this.rqid)) {
|
|
return;
|
|
}
|
|
|
|
if (side.decision && side.decision.finalDecision) {
|
|
this.debug("Can't override decision: the last pokemon could have been trapped or disabled");
|
|
return;
|
|
}
|
|
|
|
side.decision = this.parseChoice(choice.split(','), side);
|
|
side.choice = choice;
|
|
|
|
if (this.p1.decision && this.p2.decision) {
|
|
this.commitDecisions();
|
|
}
|
|
};
|
|
Battle.prototype.commitDecisions = function () {
|
|
let oldQueue = this.queue;
|
|
this.queue = [];
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
this.sides[i].resolveDecision();
|
|
if (this.sides[i].decision === true) continue;
|
|
this.addQueue(this.sides[i].decision);
|
|
}
|
|
this.add('choice', this.p1.getChoice, this.p2.getChoice);
|
|
|
|
this.sortQueue();
|
|
Array.prototype.push.apply(this.queue, oldQueue);
|
|
|
|
this.currentRequest = '';
|
|
this.currentRequestDetails = '';
|
|
this.p1.currentRequest = '';
|
|
this.p2.currentRequest = '';
|
|
|
|
this.p1.decision = true;
|
|
this.p2.decision = true;
|
|
|
|
this.go();
|
|
};
|
|
Battle.prototype.undoChoice = function (sideid) {
|
|
let side = null;
|
|
if (sideid === 'p1' || sideid === 'p2') side = this[sideid];
|
|
// The following condition can never occur for the reasons given in
|
|
// the choose() function above.
|
|
if (!side) return; // wtf
|
|
// This condition can occur.
|
|
if (!side.currentRequest) return;
|
|
|
|
if (side.decision && side.decision.finalDecision) {
|
|
this.debug("Can't cancel decision: the last pokemon could have been trapped or disabled");
|
|
return;
|
|
}
|
|
|
|
side.decision = false;
|
|
};
|
|
/**
|
|
* Parses a choice string passed from a client into a decision object
|
|
* usable by PS's engine.
|
|
*
|
|
* Choice validation is also done here.
|
|
*/
|
|
Battle.prototype.parseChoice = function (choices, side) {
|
|
let prevSwitches = {};
|
|
if (!side.currentRequest) return true;
|
|
|
|
if (typeof choices === 'string') choices = choices.split(',');
|
|
|
|
let decisions = [];
|
|
let len = choices.length;
|
|
if (side.currentRequest !== 'teampreview') len = side.active.length;
|
|
|
|
let isDefault;
|
|
let choosableTargets = {normal:1, any:1, adjacentAlly:1, adjacentAllyOrSelf:1, adjacentFoe:1};
|
|
|
|
let freeSwitchCount = {'switch':0, 'pass':0};
|
|
if (side.currentRequest === 'switch') {
|
|
let canSwitch = side.active.filter(function (mon) {return mon && mon.switchFlag;}).length;
|
|
freeSwitchCount['switch'] = Math.min(canSwitch, side.pokemon.slice(side.active.length).filter(function (mon) {return !mon.fainted;}).length);
|
|
freeSwitchCount['pass'] = canSwitch - freeSwitchCount['switch'];
|
|
}
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
let choice = (choices[i] || '').trim();
|
|
|
|
let data = '';
|
|
let firstSpaceIndex = choice.indexOf(' ');
|
|
if (firstSpaceIndex >= 0) {
|
|
data = choice.substr(firstSpaceIndex + 1).trim();
|
|
choice = choice.substr(0, firstSpaceIndex).trim();
|
|
}
|
|
|
|
let pokemon = side.pokemon[i];
|
|
|
|
switch (side.currentRequest) {
|
|
case 'teampreview':
|
|
if (choice !== 'team' || i > 0) return false;
|
|
break;
|
|
case 'move': {
|
|
if (i >= side.active.length) return false;
|
|
if (!pokemon || pokemon.fainted) {
|
|
decisions.push({
|
|
choice: 'pass'
|
|
});
|
|
continue;
|
|
}
|
|
let lockedMove = pokemon.getLockedMove();
|
|
if (lockedMove) {
|
|
decisions.push({
|
|
choice: 'move',
|
|
pokemon: pokemon,
|
|
targetLoc: this.runEvent('LockMoveTarget', pokemon) || 0,
|
|
move: lockedMove
|
|
});
|
|
continue;
|
|
}
|
|
if (isDefault || choice === 'default') {
|
|
isDefault = true;
|
|
let moves = pokemon.getMoves();
|
|
let moveid = 'struggle';
|
|
for (let j = 0; j < moves.length; j++) {
|
|
if (moves[j].disabled) continue;
|
|
moveid = moves[j].id;
|
|
break;
|
|
}
|
|
decisions.push({
|
|
choice: 'move',
|
|
pokemon: pokemon,
|
|
targetLoc: 0,
|
|
move: moveid
|
|
});
|
|
continue;
|
|
}
|
|
if (choice !== 'move' && choice !== 'switch' && choice !== 'shift') {
|
|
if (i === 0) return false;
|
|
// fallback
|
|
choice = 'move';
|
|
data = '1';
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'switch':
|
|
if (i >= side.active.length) return false;
|
|
if (!side.active[i] || !side.active[i].switchFlag) {
|
|
if (choice !== 'pass') choices.splice(i, 0, 'pass');
|
|
decisions.push({
|
|
choice: 'pass',
|
|
pokemon: side.active[i],
|
|
priority: 102
|
|
});
|
|
continue;
|
|
}
|
|
if (choice !== 'switch' && choice !== 'pass') return false;
|
|
freeSwitchCount[choice]--;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
switch (choice) {
|
|
case 'team': {
|
|
let pokemonLength = side.pokemon.length;
|
|
if (!data || data.length > pokemonLength) return false;
|
|
|
|
let dataArr = [0, 1, 2, 3, 4, 5].slice(0, pokemonLength);
|
|
let slotMap = dataArr.slice(); // Inverse of `dataArr` (slotMap[dataArr[x]] === x)
|
|
|
|
for (let j = 0; j < data.length; j++) {
|
|
let slot = parseInt(data.charAt(j), 10) - 1;
|
|
if (slotMap[slot] < j) return false;
|
|
if (isNaN(slot) || slot < 0 || slot >= pokemonLength) return false;
|
|
|
|
// Keep track of team order so far
|
|
let tempSlot = dataArr[j];
|
|
dataArr[j] = slot;
|
|
dataArr[slotMap[slot]] = tempSlot;
|
|
|
|
// Update its inverse
|
|
slotMap[tempSlot] = slotMap[slot];
|
|
slotMap[slot] = j;
|
|
}
|
|
|
|
decisions.push({
|
|
choice: 'team',
|
|
side: side,
|
|
team: dataArr
|
|
});
|
|
break;
|
|
}
|
|
|
|
case 'switch':
|
|
if (i > side.active.length || i > side.pokemon.length) continue;
|
|
|
|
data = parseInt(data, 10) - 1;
|
|
if (data < 0) data = 0;
|
|
if (data > side.pokemon.length - 1) data = side.pokemon.length - 1;
|
|
|
|
if (!side.pokemon[data]) {
|
|
this.debug("Can't switch: You can't switch to a pokemon that doesn't exist");
|
|
return false;
|
|
}
|
|
if (data === i) {
|
|
this.debug("Can't switch: You can't switch to yourself");
|
|
return false;
|
|
}
|
|
if (data < side.active.length) {
|
|
this.debug("Can't switch: You can't switch to an active pokemon");
|
|
return false;
|
|
}
|
|
if (side.pokemon[data].fainted) {
|
|
this.debug("Can't switch: You can't switch to a fainted pokemon");
|
|
return false;
|
|
}
|
|
if (prevSwitches[data]) {
|
|
this.debug("Can't switch: You can't switch to pokemon already queued to be switched");
|
|
return false;
|
|
}
|
|
prevSwitches[data] = true;
|
|
|
|
if (side.currentRequest === 'move') {
|
|
if (side.pokemon[i].trapped) {
|
|
//this.debug("Can't switch: The active pokemon is trapped");
|
|
side.emitCallback('trapped', i);
|
|
return false;
|
|
} else if (side.pokemon[i].maybeTrapped) {
|
|
let finalDecision = true;
|
|
decisions.finalDecision = decisions.finalDecision || side.pokemon[i].isLastActive();
|
|
}
|
|
}
|
|
|
|
decisions.push({
|
|
choice: (side.currentRequest === 'switch' ? 'instaswitch' : 'switch'),
|
|
pokemon: side.pokemon[i],
|
|
target: side.pokemon[data]
|
|
});
|
|
break;
|
|
|
|
case 'shift':
|
|
if (i > side.active.length || i > side.pokemon.length) continue;
|
|
if (this.gameType !== 'triples') {
|
|
this.debug("Can't shift: You can't shift a pokemon to the center except in a triple battle");
|
|
return false;
|
|
}
|
|
if (i === 1) {
|
|
this.debug("Can't shift: You can't shift a pokemon to its own position");
|
|
return false;
|
|
}
|
|
|
|
decisions.push({
|
|
choice: 'shift',
|
|
pokemon: side.pokemon[i]
|
|
});
|
|
break;
|
|
|
|
case 'move': {
|
|
let moveid = '';
|
|
let targetLoc = 0;
|
|
pokemon = side.pokemon[i];
|
|
|
|
if (data.substr(-2) === ' 1') targetLoc = 1;
|
|
if (data.substr(-2) === ' 2') targetLoc = 2;
|
|
if (data.substr(-2) === ' 3') targetLoc = 3;
|
|
if (data.substr(-3) === ' -1') targetLoc = -1;
|
|
if (data.substr(-3) === ' -2') targetLoc = -2;
|
|
if (data.substr(-3) === ' -3') targetLoc = -3;
|
|
|
|
if (targetLoc) data = data.substr(0, data.lastIndexOf(' '));
|
|
|
|
if (data.substr(-5) === ' mega') {
|
|
decisions.push({
|
|
choice: 'megaEvo',
|
|
pokemon: pokemon
|
|
});
|
|
data = data.substr(0, data.length - 5);
|
|
}
|
|
|
|
/**
|
|
* Parse the move identifier (name or index), according to the request sent to the client.
|
|
* If the move is not found, the decision is invalid without requiring further inspection.
|
|
*/
|
|
|
|
let requestMoves = pokemon.getRequestData().moves;
|
|
if (data.search(/^[0-9]+$/) >= 0) {
|
|
// parse a one-based move index
|
|
let moveIndex = parseInt(data, 10) - 1;
|
|
if (!requestMoves[moveIndex]) {
|
|
this.debug("Can't use an unexpected move");
|
|
return false;
|
|
}
|
|
moveid = requestMoves[moveIndex].id;
|
|
if (!targetLoc && side.active.length > 1 && requestMoves[moveIndex].target in choosableTargets) {
|
|
this.debug("Can't use the move without a target");
|
|
return false;
|
|
}
|
|
} else {
|
|
// parse a move name
|
|
moveid = toId(data);
|
|
if (moveid.substr(0, 11) === 'hiddenpower') {
|
|
moveid = 'hiddenpower';
|
|
}
|
|
let isValidMove = false;
|
|
for (let j = 0; j < requestMoves.length; j++) {
|
|
if (requestMoves[j].id !== moveid) continue;
|
|
if (!targetLoc && side.active.length > 1 && requestMoves[j].target in choosableTargets) {
|
|
this.debug("Can't use the move without a target");
|
|
return false;
|
|
}
|
|
isValidMove = true;
|
|
break;
|
|
}
|
|
if (!isValidMove) {
|
|
this.debug("Can't use an unexpected move");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check whether the chosen move is really valid, accounting for effects active in battle,
|
|
* which could be unknown for the client.
|
|
*/
|
|
|
|
let moves = pokemon.getMoves();
|
|
if (!moves.length) {
|
|
// Override decision and use Struggle if there are no enabled moves with PP
|
|
if (this.gen <= 4) side.send('-activate', pokemon, 'move: Struggle');
|
|
moveid = 'struggle';
|
|
} else {
|
|
// At least a move is valid. Check if the chosen one is.
|
|
// This may include Struggle in Hackmons.
|
|
let isEnabled = false;
|
|
for (let j = 0; j < moves.length; j++) {
|
|
if (moves[j].id !== moveid) continue;
|
|
if (!moves[j].disabled) {
|
|
isEnabled = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!isEnabled) {
|
|
// request a different choice
|
|
let sourceEffect = pokemon.disabledMoves[moveid] && pokemon.disabledMoves[moveid].sourceEffect;
|
|
side.emitCallback('cant', pokemon, sourceEffect ? sourceEffect.fullname : '', moveid);
|
|
return false;
|
|
}
|
|
// the chosen move is valid
|
|
}
|
|
|
|
if (pokemon.maybeDisabled) {
|
|
decisions.finalDecision = decisions.finalDecision || pokemon.isLastActive();
|
|
}
|
|
|
|
decisions.push({
|
|
choice: 'move',
|
|
pokemon: pokemon,
|
|
targetLoc: targetLoc,
|
|
move: moveid
|
|
});
|
|
break;
|
|
}
|
|
|
|
case 'pass':
|
|
if (i > side.active.length || i > side.pokemon.length) continue;
|
|
if (side.currentRequest !== 'switch') {
|
|
this.debug("Can't pass the turn");
|
|
return false;
|
|
}
|
|
decisions.push({
|
|
choice: 'pass',
|
|
priority: 102,
|
|
pokemon: side.active[i]
|
|
});
|
|
}
|
|
}
|
|
if (freeSwitchCount['switch'] !== 0 || freeSwitchCount['pass'] !== 0) return false;
|
|
|
|
if (!this.supportCancel || isDefault) decisions.finalDecision = true;
|
|
return decisions;
|
|
};
|
|
Battle.prototype.add = function () {
|
|
let parts = Array.prototype.slice.call(arguments);
|
|
let functions = parts.map(function (part) {
|
|
return typeof part === 'function';
|
|
});
|
|
if (functions.indexOf(true) < 0) {
|
|
this.log.push('|' + parts.join('|'));
|
|
} else {
|
|
this.log.push('|split');
|
|
let sides = [null, this.sides[0], this.sides[1], true];
|
|
for (let i = 0; i < sides.length; ++i) {
|
|
let line = '';
|
|
for (let j = 0; j < parts.length; ++j) {
|
|
line += '|';
|
|
if (functions[j]) {
|
|
line += parts[j](sides[i]);
|
|
} else {
|
|
line += parts[j];
|
|
}
|
|
}
|
|
this.log.push(line);
|
|
}
|
|
}
|
|
};
|
|
Battle.prototype.addMove = function () {
|
|
this.lastMoveLine = this.log.length;
|
|
this.log.push('|' + Array.prototype.slice.call(arguments).join('|'));
|
|
};
|
|
Battle.prototype.attrLastMove = function () {
|
|
this.log[this.lastMoveLine] += '|' + Array.prototype.slice.call(arguments).join('|');
|
|
};
|
|
Battle.prototype.debug = function (activity) {
|
|
if (this.getFormat().debug) {
|
|
this.add('debug', activity);
|
|
}
|
|
};
|
|
Battle.prototype.debugError = function (activity) {
|
|
this.add('debug', activity);
|
|
};
|
|
|
|
// players
|
|
|
|
Battle.prototype.join = function (slot, name, avatar, team) {
|
|
if (this.p1 && this.p1.isActive && this.p2 && this.p2.isActive) return false;
|
|
if ((this.p1 && this.p1.isActive && this.p1.name === name) || (this.p2 && this.p2.isActive && this.p2.name === name)) return false;
|
|
if (this.p1 && this.p1.isActive || slot === 'p2') {
|
|
if (this.started) {
|
|
this.p2.name = name;
|
|
} else {
|
|
//console.log("NEW SIDE: " + name);
|
|
this.p2 = new BattleSide(name, this, 1, team);
|
|
this.sides[1] = this.p2;
|
|
}
|
|
if (avatar) this.p2.avatar = avatar;
|
|
this.p2.isActive = true;
|
|
this.add('player', 'p2', this.p2.name, avatar);
|
|
} else {
|
|
if (this.started) {
|
|
this.p1.name = name;
|
|
} else {
|
|
//console.log("NEW SIDE: " + name);
|
|
this.p1 = new BattleSide(name, this, 0, team);
|
|
this.sides[0] = this.p1;
|
|
}
|
|
if (avatar) this.p1.avatar = avatar;
|
|
this.p1.isActive = true;
|
|
this.add('player', 'p1', this.p1.name, avatar);
|
|
}
|
|
this.start();
|
|
return true;
|
|
};
|
|
Battle.prototype.rename = function (slot, name, avatar) {
|
|
if (slot === 'p1' || slot === 'p2') {
|
|
let side = this[slot];
|
|
side.name = name;
|
|
if (avatar) side.avatar = avatar;
|
|
this.add('player', slot, name, side.avatar);
|
|
}
|
|
};
|
|
Battle.prototype.leave = function (slot) {
|
|
if (slot === 'p1' || slot === 'p2') {
|
|
let side = this[slot];
|
|
if (!side) {
|
|
console.log('**** ' + slot + ' tried to leave before it was possible in ' + this.id);
|
|
require('./crashlogger.js')({stack: '**** ' + slot + ' tried to leave before it was possible in ' + this.id}, 'A simulator process');
|
|
return;
|
|
}
|
|
|
|
side.emitRequest(null);
|
|
side.isActive = false;
|
|
this.add('player', slot);
|
|
this.active = false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// IPC
|
|
|
|
// Messages sent by this function are received and handled in
|
|
// Battle.prototype.receive in simulator.js (in another process).
|
|
Battle.prototype.send = function (type, data) {
|
|
if (Array.isArray(data)) data = data.join("\n");
|
|
process.send(this.id + "\n" + type + "\n" + data);
|
|
};
|
|
// This function is called by this process's 'message' event.
|
|
Battle.prototype.receive = function (data, more) {
|
|
this.messageLog.push(data.join(' '));
|
|
let logPos = this.log.length;
|
|
let alreadyEnded = this.ended;
|
|
switch (data[1]) {
|
|
case 'join': {
|
|
let team = '';
|
|
try {
|
|
if (more) team = Tools.fastUnpackTeam(more);
|
|
} catch (e) {
|
|
console.log('TEAM PARSE ERROR: ' + more);
|
|
team = null;
|
|
}
|
|
this.join(data[2], data[3], data[4], team);
|
|
break;
|
|
}
|
|
|
|
case 'rename':
|
|
this.rename(data[2], data[3], data[4]);
|
|
break;
|
|
|
|
case 'leave':
|
|
this.leave(data[2]);
|
|
break;
|
|
|
|
case 'chat':
|
|
this.add('chat', data[2], more);
|
|
break;
|
|
|
|
case 'win':
|
|
case 'tie':
|
|
this.win(data[2]);
|
|
break;
|
|
|
|
case 'choose':
|
|
this.choose(data[2], data[3], data[4]);
|
|
break;
|
|
|
|
case 'undo':
|
|
this.undoChoice(data[2]);
|
|
break;
|
|
|
|
case 'eval': {
|
|
let battle = this;
|
|
let p1 = this.p1;
|
|
let p2 = this.p2;
|
|
let p1active = p1 ? p1.active[0] : null;
|
|
let p2active = p2 ? p2.active[0] : null;
|
|
let target = data.slice(2).join('|').replace(/\f/g, '\n');
|
|
this.add('', '>>> ' + target);
|
|
try {
|
|
this.add('', '<<< ' + eval(target));
|
|
} catch (e) {
|
|
this.add('', '<<< error: ' + e.message);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
// unhandled
|
|
}
|
|
|
|
this.sendUpdates(logPos, alreadyEnded);
|
|
};
|
|
Battle.prototype.sendUpdates = function (logPos, alreadyEnded) {
|
|
if (this.p1 && this.p2) {
|
|
let inactiveSide = -1;
|
|
if (!this.p1.isActive && this.p2.isActive) {
|
|
inactiveSide = 0;
|
|
} else if (this.p1.isActive && !this.p2.isActive) {
|
|
inactiveSide = 1;
|
|
} else if (!this.p1.decision && this.p2.decision) {
|
|
inactiveSide = 0;
|
|
} else if (this.p1.decision && !this.p2.decision) {
|
|
inactiveSide = 1;
|
|
}
|
|
if (inactiveSide !== this.inactiveSide) {
|
|
this.send('inactiveside', inactiveSide);
|
|
this.inactiveSide = inactiveSide;
|
|
}
|
|
}
|
|
|
|
if (this.log.length > logPos) {
|
|
if (alreadyEnded !== undefined && this.ended && !alreadyEnded) {
|
|
if (this.rated || Config.logchallenges) {
|
|
let log = {
|
|
seed: this.startingSeed,
|
|
turns: this.turn,
|
|
p1: this.p1.name,
|
|
p2: this.p2.name,
|
|
p1team: this.p1.team,
|
|
p2team: this.p2.team,
|
|
log: this.log
|
|
};
|
|
this.send('log', JSON.stringify(log));
|
|
}
|
|
this.send('score', [this.p1.pokemonLeft, this.p2.pokemonLeft]);
|
|
this.send('winupdate', [this.winner].concat(this.log.slice(logPos)));
|
|
} else {
|
|
this.send('update', this.log.slice(logPos));
|
|
}
|
|
}
|
|
};
|
|
|
|
Battle.prototype.destroy = function () {
|
|
// deallocate ourself
|
|
|
|
// deallocate children and get rid of references to them
|
|
for (let i = 0; i < this.sides.length; i++) {
|
|
if (this.sides[i]) this.sides[i].destroy();
|
|
this.sides[i] = null;
|
|
}
|
|
this.p1 = null;
|
|
this.p2 = null;
|
|
for (let i = 0; i < this.queue.length; i++) {
|
|
delete this.queue[i].pokemon;
|
|
delete this.queue[i].side;
|
|
this.queue[i] = null;
|
|
}
|
|
this.queue = null;
|
|
|
|
// in case the garbage collector really sucks, at least deallocate the log
|
|
this.log = null;
|
|
|
|
// remove from battle list
|
|
Battles[this.id] = null;
|
|
};
|
|
return Battle;
|
|
})();
|
|
|
|
exports.BattlePokemon = BattlePokemon;
|
|
exports.BattleSide = BattleSide;
|
|
exports.Battle = Battle;
|