pokemon-showdown/sim/battle.js
2017-06-21 20:00:17 -07:00

2734 lines
88 KiB
JavaScript

/**
* Simulator Battle
* Pokemon Showdown - http://pokemonshowdown.com/
*
* @license MIT license
*/
'use strict';
const Dex = require('./dex');
const PRNG = require('./prng');
const Sim = require('./');
class Battle extends Dex.ModdedDex {
/**
* Initialises a Battle.
*
* @param {object} format
* @param {boolean} rated
* @param {Function} send
* @param {PRNG} [maybePrng]
*/
init(format, rated = false, send = (() => {}), prng = new PRNG()) {
this.log = [];
/** @type {Sim.Side[]} */
this.sides = [null, null];
this.rated = rated;
this.weatherData = {id:''};
this.terrainData = {id:''};
this.pseudoWeather = {};
this.format = toId(format);
this.formatData = {id:this.format};
Dex.mod(format.mod).getBanlistTable(format); // fill in format ruleset
this.ruleset = format.ruleset;
this.effect = {id:''};
this.effectData = {id:''};
this.event = {id:''};
this.gameType = (format.gameType || 'singles');
this.reportExactHP = !!format.debug;
this.replayExactHP = !format.team;
this.queue = [];
this.faintQueue = [];
this.messageLog = [];
this.send = send;
this.turn = 0;
/** @type {Sim.Side} */
this.p1 = null;
/** @type {Sim.Side} */
this.p2 = null;
this.lastUpdate = 0;
this.weather = '';
this.terrain = '';
this.ended = false;
this.started = false;
this.active = false;
this.eventDepth = 0;
this.lastMove = '';
this.activeMove = null;
this.activePokemon = null;
this.activeTarget = null;
this.midTurn = false;
this.currentRequest = '';
this.lastMoveLine = 0;
this.reportPercentages = false;
this.supportCancel = false;
this.events = null;
this.abilityOrder = 0;
this.prng = prng;
this.prngSeed = this.prng.startingSeed.slice();
}
static logReplay(data, isReplay) {
if (isReplay === true) return data;
return '';
}
toString() {
return 'Battle: ' + this.format;
}
random(m, n) {
return this.prng.next(m, n);
}
resetRNG() {
this.prng = new PRNG(this.prng.startingSeed);
}
setWeather(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) {
if (sourceEffect && sourceEffect.effectType === 'Ability') {
if (this.gen > 5 || this.weatherData.duration === 0) {
return false;
}
} else if (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;
}
return true;
}
clearWeather() {
return this.setWeather('');
}
effectiveWeather(target) {
if (this.event) {
if (!target) target = this.event.target;
}
if (this.suppressingWeather()) return '';
return this.weather;
}
isWeather(weather, target) {
let ourWeather = this.effectiveWeather(target);
if (!Array.isArray(weather)) {
return ourWeather === toId(weather);
}
return weather.map(toId).includes(ourWeather);
}
getWeather() {
return this.getEffect(this.weather);
}
setTerrain(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;
}
return true;
}
clearTerrain() {
return this.setTerrain('');
}
effectiveTerrain(target) {
if (this.event) {
if (!target) target = this.event.target;
}
if (!this.runEvent('TryTerrain', target)) return '';
return this.terrain;
}
isTerrain(terrain, target) {
let ourTerrain = this.effectiveTerrain(target);
if (!Array.isArray(terrain)) {
return ourTerrain === toId(terrain);
}
return terrain.map(toId).includes(ourTerrain);
}
getTerrain() {
return this.getEffect(this.terrain);
}
getFormat() {
return this.getEffect(this.format);
}
addPseudoWeather(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;
}
return true;
}
getPseudoWeather(status) {
status = this.getEffect(status);
if (!this.pseudoWeather[status.id]) return null;
return status;
}
removePseudoWeather(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];
return true;
}
suppressingAttackEvents() {
return this.activePokemon && this.activePokemon.isActive && this.activeMove && this.activeMove.ignoreAbility;
}
suppressingWeather() {
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;
}
setActiveMove(move, pokemon, target) {
if (!move) move = null;
if (!pokemon) pokemon = null;
if (!target) target = pokemon;
this.activeMove = move;
this.activePokemon = pokemon;
this.activeTarget = target;
}
clearActiveMove(failed) {
if (this.activeMove) {
if (!failed) {
this.lastMove = this.activeMove.id;
}
this.activeMove = null;
this.activePokemon = null;
this.activeTarget = null;
}
}
updateSpeed() {
let actives = this.p1.active;
for (let i = 0; i < actives.length; i++) {
if (actives[i]) actives[i].updateSpeed();
}
actives = this.p2.active;
for (let i = 0; i < actives.length; i++) {
if (actives[i]) actives[i].updateSpeed();
}
}
static comparePriority(a, b) {
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;
}
static compareRedirectOrder(a, b) {
a.priority = a.priority || 0;
a.speed = a.speed || 0;
b.priority = b.priority || 0;
b.speed = b.speed || 0;
if (b.priority - a.priority) {
return b.priority - a.priority;
}
if (b.speed - a.speed) {
return b.speed - a.speed;
}
if (b.thing.abilityOrder - a.thing.abilityOrder) {
return -(b.thing.abilityOrder - a.thing.abilityOrder);
}
return 0;
}
getResidualStatuses(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;
}
eachEvent(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((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++) {
this.runEvent(eventid, actives[i], null, effect, relayVar);
}
}
residualEvent(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 * )
singleEvent(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' && eventid !== 'Primal' && effect.effectType === 'Item' && (target instanceof Sim.Pokemon) && target.ignoringItem()) {
this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room');
return relayVar;
}
if (eventid !== 'End' && effect.effectType === 'Ability' && (target instanceof Sim.Pokemon) && 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 Dex, 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.
*/
runEvent(eventid, target, source, effect, relayVar, onEffect, fastExit) {
// if (Battle.eventCounter) {
// if (!Battle.eventCounter[eventid]) Battle.eventCounter[eventid] = 0;
// Battle.eventCounter[eventid]++;
// }
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);
if (fastExit) {
statuses.sort(Battle.compareRedirectOrder);
} else {
statuses.sort(Battle.comparePriority);
}
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' && !status.isUnbreakable && this.suppressingAttackEvents() && this.activePokemon !== thing) {
// ignore attacking events
let AttackingEvents = {
BeforeMove: 1,
BasePower: 1,
Immunity: 1,
RedirectTarget: 1,
Heal: 1,
SetStatus: 1,
CriticalHit: 1,
ModifyAtk: 1, ModifyDef: 1, ModifySpA: 1, ModifySpD: 1, ModifySpe: 1, ModifyAccuracy: 1,
ModifyBoost: 1,
ModifyDamage: 1,
ModifySecondaries: 1,
ModifyWeight: 1,
TryAddVolatile: 1,
TryHit: 1,
TryHitSide: 1,
TryMove: 1,
Boost: 1,
DragOut: 1,
Effectiveness: 1,
};
if (eventid in AttackingEvents) {
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 !== 'SwitchIn' && eventid !== 'TakeItem' && status.effectType === 'Item' && (thing instanceof Sim.Pokemon) && thing.ignoringItem()) {
if (eventid !== 'Update') {
this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room');
}
continue;
} else if (eventid !== 'End' && status.effectType === 'Ability' && (thing instanceof Sim.Pokemon) && thing.ignoringAbility()) {
if (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 || fastExit) 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;
}
/**
* priorityEvent works just like runEvent, except it exits and returns
* on the first non-undefined value instead of only on null/false.
*/
priorityEvent(eventid, target, source, effect, relayVar, onEffect) {
return this.runEvent(eventid, target, source, effect, relayVar, onEffect, true);
}
resolveLastPriority(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
getRelevantEffects(thing, callbackType, foeCallbackType, foeThing) {
let statuses = this.getRelevantEffectsInner(thing, callbackType, foeCallbackType, foeThing, true, false);
//if (statuses[0]) this.debug('match ' + callbackType + ': ' + statuses[0].status.id);
return statuses;
}
getRelevantEffectsInner(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.onEvent(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.onEvent(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)
*/
onEvent(eventid, target, ...rest) { // rest = [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 (!rest.length) throw new TypeError("Event handlers must have a callback");
if (target.effectType !== 'Format') {
throw new TypeError(`${target.name} is a ${target.effectType} but only Format targets are supported right now`);
}
let callback, priority, order, subOrder, data;
if (rest.length === 1) {
[callback] = rest;
priority = 0;
order = false;
subOrder = 0;
} else {
[data, callback] = rest;
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, target, priority, order, 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);
}
}
getPokemon(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;
}
makeRequest(type) {
if (type) {
this.currentRequest = type;
this.p1.clearChoice();
this.p2.clearChoice();
} else {
type = this.currentRequest;
}
// default to no request
/** @type {any} */
let p1request = null;
/** @type {any} */
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.some(flag => flag === true)) {
this.p1.currentRequest = 'switch';
p1request = {forceSwitch: switchTable, side: this.p1.getData()};
}
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.some(flag => flag === true)) {
this.p2.currentRequest = 'switch';
p2request = {forceSwitch: switchTable, side: this.p2.getData()};
}
break;
}
case 'teampreview':
let maxTeamSize = 6;
let teamLengthData = this.getFormat().teamLength;
if (teamLengthData && teamLengthData.battle) maxTeamSize = teamLengthData.battle;
this.p1.maxTeamSize = maxTeamSize;
this.p2.maxTeamSize = maxTeamSize;
this.add('teampreview' + (maxTeamSize !== 6 ? '|' + maxTeamSize : ''));
this.p1.currentRequest = 'teampreview';
p1request = {teamPreview: true, maxTeamSize: maxTeamSize, side: this.p1.getData()};
this.p2.currentRequest = 'teampreview';
p2request = {teamPreview: true, maxTeamSize: maxTeamSize, side: this.p2.getData()};
break;
default: {
this.p1.currentRequest = 'move';
let activeData = this.p1.active.map(pokemon => pokemon && pokemon.getRequestData());
p1request = {active: activeData, side: this.p1.getData()};
this.p2.currentRequest = 'move';
activeData = this.p2.active.map(pokemon => pokemon && pokemon.getRequestData());
p2request = {active: activeData, side: this.p2.getData()};
break;
}
}
if (p1request) {
if (!this.supportCancel || !p2request) p1request.noCancel = true;
this.p1.emitRequest(p1request);
} else {
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.emitRequest({wait: true, side: this.p2.getData()});
}
if (this.p1.isChoiceDone() && this.p2.isChoiceDone()) {
throw new Error(`Choices are done immediately after a request`);
}
}
tie() {
this.win();
}
win(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 = '';
for (let side of this.sides) {
side.currentRequest = '';
}
return true;
}
switchIn(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);
this.insertQueue({pokemon: pokemon, choice: 'runUnnerve'});
this.insertQueue({pokemon: pokemon, choice: 'runSwitch'});
}
canSwitch(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;
}
getRandomSwitchable(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)];
}
dragIn(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);
oldActive.illusion = null;
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);
if (this.gen >= 5) {
this.singleEvent('PreStart', pokemon.getAbility(), pokemon.abilityData, pokemon);
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;
}
swapPosition(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;
}
faint(pokemon, source, effect) {
pokemon.faint(source, effect);
}
nextTurn() {
this.turn++;
let allStale = true;
/** @type {Sim.Pokemon} */
let oneStale;
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.maybeDisabled = false;
for (let entry of pokemon.moveset) {
entry.disabled = false;
entry.disabledSource = '';
}
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;
}
}
pokemon.trapped = pokemon.maybeTrapped = false;
this.runEvent('TrapPokemon', pokemon);
if (!pokemon.knownType || this.getImmunity('trapped', pokemon)) {
this.runEvent('MaybeTrapPokemon', pokemon);
}
// Disable the faculty to cancel switches if a foe may have a trapping ability
let foeSide = pokemon.side.foe;
for (let k = 0; k < foeSide.active.length; ++k) {
let source = foeSide.active[k];
if (!source || source.fainted) continue;
let template = (source.illusion || source).template;
if (!template.abilities) continue;
for (let abilitySlot in template.abilities) {
let abilityName = template.abilities[abilitySlot];
if (abilityName === source.ability) {
// pokemon event was already run above so we don't need
// to run it again.
continue;
}
let banlistTable = this.getFormat().banlistTable;
if (banlistTable && !('illegal' in banlistTable) && !this.getFormat().team) {
// hackmons format
continue;
} else if (abilitySlot === 'H' && template.unreleasedHidden) {
// unreleased hidden ability
continue;
}
let ability = this.getAbility(abilityName);
if (banlistTable && ability.id in banlistTable) continue;
if (pokemon.knownType && !this.getImmunity('trapped', pokemon)) continue;
this.singleEvent('FoeMaybeTrapPokemon',
ability, {}, pokemon, source);
}
}
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">' + Chat.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">' + Chat.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">' + Chat.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">' + Chat.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&eacute;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">' + Chat.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">' + Chat.escapeHTML(oneStale.name) + ' is in an endless loop.</div>');
oneStale.staleWarned = true;
}
}
if (this.gameType === 'triples' && !this.sides.filter(side => side.pokemonLeft > 1).length) {
// If both sides have one Pokemon left in triples and they are not adjacent, they are both moved to the center.
let actives = [];
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;
actives.push(this.sides[i].active[j]);
}
}
if (actives.length > 1 && !this.isAdjacent(actives[0], actives[1])) {
this.swapPosition(actives[0], 1, '[silent]');
this.swapPosition(actives[1], 1, '[silent]');
this.add('-center');
}
}
this.add('turn', this.turn);
this.makeRequest('move');
}
start() {
if (this.active) return;
if (!this.p1 || !this.p2) {
// need two players to start
return;
}
if (this.started) {
return;
}
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();
this.add('tier', format.name);
if (this.rated) {
this.add('rated');
}
this.add('seed', side => Battle.logReplay(this.prngSeed.join(','), side));
if (format.onBegin) {
format.onBegin.call(this);
}
if (this.ruleset) {
for (let i = 0; i < this.ruleset.length; i++) {
this.addPseudoWeather(this.ruleset[i]);
}
}
if (!this.p1.pokemon[0] || !this.p2.pokemon[0]) {
throw new Error('Battle not started: A player has an empty team.');
}
this.residualEvent('TeamPreview');
this.addQueue({choice: 'start'});
this.midTurn = true;
if (!this.currentRequest) this.go();
}
boost(boost, target, source, effect, isSecondary, isSelf) {
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;
if (this.gen > 5 && !target.side.foe.pokemonLeft) return false;
effect = this.getEffect(effect);
boost = this.runEvent('Boost', target, source, effect, Object.assign({}, boost));
let success = null;
let boosted = false;
for (let i in boost) {
let currentBoost = {};
currentBoost[i] = boost[i];
let boostBy = target.boostBy(currentBoost);
let msg = '-boost';
if (boost[i] < 0) {
msg = '-unboost';
boostBy = -boostBy;
}
if (boostBy) {
success = true;
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, boostBy, '[silent]');
this.add('-hint', "In Gen 2, Belly Drum boosts by 2 when it fails.");
break;
case 'intimidate': case 'gooey': case 'tanglinghair':
this.add(msg, target, i, boostBy);
break;
case 'zpower':
this.add(msg, target, i, boostBy, '[zeffect]');
break;
default:
if (effect.effectType === 'Move') {
this.add(msg, target, i, boostBy);
} else {
if (effect.effectType === 'Ability' && !boosted) {
this.add('-ability', target, effect.name, 'boost');
boosted = true;
}
this.add(msg, target, i, boostBy);
}
break;
}
this.runEvent('AfterEachBoost', target, source, effect, currentBoost);
} else if (effect.effectType === 'Ability') {
if (isSecondary) this.add(msg, target, i, boostBy);
} else if (!isSecondary && !isSelf) {
this.add(msg, target, i, boostBy);
}
}
this.runEvent('AfterBoost', target, source, effect, boost);
return success;
}
damage(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.runStatusImmunity(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 (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' || !name) {
this.add('-damage', target, target.getHealth);
} else if (source && (source !== target || effect.effectType === 'Ability')) {
this.add('-damage', target, target.getHealth, '[from] ' + name, '[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(entry => entry.target).map(pokemon => pokemon.name));
this.faintMessages(true);
} else {
damage = this.runEvent('AfterDamage', target, source, effect, damage);
}
return damage;
}
directDamage(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;
}
heal(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;
case 'zpower':
this.add('-heal', target, target.getHealth, '[zeffect]');
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;
}
chain(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
}
chainModify(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;
}
modify(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);
}
getCategory(move) {
move = this.getMove(move);
return move.category || 'Physical';
}
getDamage(pokemon, target, move, suppressMessages) {
if (typeof move === 'string') move = this.getMove(move);
if (typeof move === 'number') {
move = {
basePower: move,
type: '???',
category: 'Physical',
willCrit: false,
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 = {};
}
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;
let critRatio = this.runEvent('ModifyCritRatio', pokemon, target, move, move.critRatio || 0);
if (this.gen <= 5) {
critRatio = this.clampIntRange(critRatio, 0, 5);
critMult = [0, 16, 8, 4, 3, 2];
} else {
critRatio = this.clampIntRange(critRatio, 0, 4);
critMult = [0, 16, 8, 2, 1];
}
move.crit = move.willCrit || false;
if (move.willCrit === undefined) {
if (critRatio) {
move.crit = (this.random(critMult[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);
// Calculate damage modifiers separately (order differs between generations)
return this.modifyDamage(baseDamage, pokemon, target, move, suppressMessages);
}
modifyDamage(baseDamage, pokemon, target, move, suppressMessages) {
if (!move.type) move.type = '???';
let type = move.type;
baseDamage += 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' && 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 && !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);
// TODO: Find out where this actually goes in the damage calculation
if (move.isZ && move.zBrokeProtect) {
baseDamage = this.modify(baseDamage, 0.25);
this.add('-message', target.name + " couldn't fully protect itself and got hurt! (placeholder)");
}
if (this.gen !== 5 && !Math.floor(baseDamage)) {
return 1;
}
return Math.floor(baseDamage);
}
randomizer(baseDamage) {
return Math.floor(baseDamage * (100 - this.random(16)) / 100);
}
/**
* Returns whether a proposed target for a move is valid.
*/
validTargetLoc(targetLoc, source, targetType) {
if (targetLoc === 0) return true;
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;
}
getTargetLoc(target, source) {
if (target.side === source.side) {
return -(target.position + 1);
} else {
return target.position + 1;
}
}
validTarget(target, source, targetType) {
return this.validTargetLoc(this.getTargetLoc(target, source), source, targetType);
}
getTarget(pokemon, move, targetLoc) {
move = this.getMove(move);
let target;
if ((move.target !== 'randomNormal') &&
this.validTargetLoc(targetLoc, pokemon, move.target)) {
if (targetLoc > 0) {
target = pokemon.side.foe.active[targetLoc - 1];
} else {
target = pokemon.side.active[-targetLoc - 1];
}
if (target) {
if (!target.fainted) {
// target exists and is not fainted
return target;
} else if (target.side === pokemon.side) {
// fainted allied targets don't retarget
return false;
}
}
// chosen target not valid, retarget randomly with resolveTarget
}
return this.resolveTarget(pokemon, move);
}
resolveTarget(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 allyActives = pokemon.side.active;
let adjacentAllies = [allyActives[pokemon.position - 1], allyActives[pokemon.position + 1]];
adjacentAllies = adjacentAllies.filter(active => 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);
adjacentFoes = adjacentFoes.filter(active => 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];
}
checkFainted() {
for (let i = 0; i < this.p1.active.length; i++) {
let pokemon = this.p1.active[i];
if (pokemon.fainted) {
pokemon.status = 'fnt';
pokemon.switchFlag = true;
}
}
for (let i = 0; i < this.p2.active.length; i++) {
let pokemon = this.p2.active[i];
if (pokemon.fainted) {
pokemon.status = 'fnt';
pokemon.switchFlag = true;
}
}
}
faintMessages(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.runEvent('BeforeFaint', faintData.target, faintData.source, faintData.effect);
this.add('faint', faintData.target);
faintData.target.side.pokemonLeft--;
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.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]);
// Stop Pursuit from running
this.p1.active[i].moveThisTurn = true;
}
for (let i = 0; i < this.p2.active.length; i++) {
this.cancelMove(this.p2.active[i]);
// Stop Pursuit from running
this.p2.active[i].moveThisTurn = true;
}
}
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;
}
resolvePriority(decision, midTurn) {
if (!decision) return;
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,
'runUnnerve': 7.3,
'runSwitch': 7.2,
'runPrimal': 7.1,
'instaswitch': 101,
'megaEvo': 6.9,
'residual': -100,
'team': 102,
'start': 101,
};
if (decision.choice in priorities) {
decision.priority = priorities[decision.choice];
}
}
if (!midTurn) {
if (decision.choice === 'move') {
if (!decision.zmove && this.getMove(decision.move).beforeTurnCallback) {
this.addQueue({choice: 'beforeTurnMove', pokemon: decision.pokemon, move: decision.move, targetLoc: decision.targetLoc});
}
if (decision.mega) {
// TODO: Check that the Pokémon is not affected by Sky Drop.
// (This is currently being done in `runMegaEvo`).
this.addQueue({
choice: 'megaEvo',
pokemon: decision.pokemon,
});
}
} 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.getDecisionSpeed();
}
}
let deferPriority = this.gen >= 7 && decision.mega && !decision.pokemon.template.isMega;
if (decision.move) {
let target;
if (!decision.targetLoc) {
target = this.resolveTarget(decision.pokemon, decision.move);
decision.targetLoc = this.getTargetLoc(target, decision.pokemon);
}
decision.move = this.getMoveCopy(decision.move);
if (!decision.priority && !deferPriority) {
let move = decision.move;
if (decision.zmove) {
let zMoveName = this.getZMove(decision.move, decision.pokemon, true);
let zMove = this.getMove(zMoveName);
if (zMove.exists) {
move = zMove;
}
}
let priority = this.runEvent('ModifyPriority', decision.pokemon, target, move, 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.getDecisionSpeed();
if (!decision.speed && !deferPriority) decision.speed = decision.pokemon.getDecisionSpeed();
}
addQueue(action) {
if (Array.isArray(action)) {
for (let i = 0; i < action.length; i++) {
this.addQueue(action[i]);
}
return;
}
if (action.choice === 'pass') return;
this.resolvePriority(action);
this.queue.push(action);
}
sortQueue() {
this.queue.sort(Battle.comparePriority);
}
insertQueue(decision, midTurn) {
if (Array.isArray(decision)) {
for (let i = 0; i < decision.length; i++) {
this.insertQueue(decision[i]);
}
return;
}
if (decision.pokemon) decision.pokemon.updateSpeed();
this.resolvePriority(decision, midTurn);
for (let i = 0; i < this.queue.length; i++) {
if (Battle.comparePriority(decision, this.queue[i]) < 0) {
this.queue.splice(i, 0, decision);
return;
}
}
this.queue.push(decision);
}
prioritizeQueue(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);
}
willAct() {
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;
}
willMove(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;
}
cancelDecision(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;
}
cancelMove(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;
}
willSwitch(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;
}
runDecision(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();
// Remove Pokémon duplicates remaining after `team` decisions.
this.p1.pokemon = this.p1.pokemon.slice(0, this.p1.pokemonLeft);
this.p2.pokemon = this.p2.pokemon.slice(0, this.p2.pokemonLeft);
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, decision.targetLoc, decision.sourceEffect, decision.zmove);
break;
case 'megaEvo':
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.pokemon, decision.move, decision.targetLoc);
if (!target) return false;
decision.move.beforeTurnCallback.call(this, decision.pokemon, target);
break;
}
case 'event':
this.runEvent(decision.event, decision.pokemon);
break;
case 'team': {
decision.side.pokemon.splice(decision.index, 0, decision.pokemon);
decision.pokemon.position = decision.index;
// 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.choice === 'switch' && decision.pokemon.status && this.data.Abilities.naturalcure) {
this.singleEvent('CheckShow', this.data.Abilities.naturalcure, null, decision.pokemon);
}
if (decision.pokemon.hp) {
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;
}
}
decision.pokemon.illusion = null;
this.singleEvent('End', this.getAbility(decision.pokemon.ability), decision.pokemon.abilityData, decision.pokemon);
if (!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.add('-hint', 'Pursuit target fainted, switch continues in gen 2-4');
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.add('-hint', 'Switch failed; 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 'runUnnerve':
this.singleEvent('PreStart', decision.pokemon.getAbility(), decision.pokemon.abilityData, decision.pokemon);
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);
decision.pokemon.abilityOrder = this.abilityOrder++;
this.singleEvent('Start', decision.pokemon.getItem(), decision.pokemon.itemData, decision.pokemon);
}
delete decision.pokemon.draggedIn;
break;
case 'runPrimal':
if (!decision.pokemon.transformed) this.singleEvent('Primal', decision.pokemon.getItem(), decision.pokemon.itemData, decision.pokemon);
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.updateSpeed();
this.residualEvent('Residual');
this.add('upkeep');
break;
case 'skip':
throw new Error("Decision illegally skipped!");
}
// phazing (Roar, etc)
for (let i = 0; i < this.p1.active.length; i++) {
let pokemon = this.p1.active[i];
if (pokemon.forceSwitchFlag) {
if (pokemon.hp) this.dragIn(pokemon.side, pokemon.position);
pokemon.forceSwitchFlag = false;
}
}
for (let i = 0; i < this.p2.active.length; i++) {
let pokemon = this.p2.active[i];
if (pokemon.forceSwitchFlag) {
if (pokemon.hp) this.dragIn(pokemon.side, pokemon.position);
pokemon.forceSwitchFlag = false;
}
}
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;
} else if (decision.choice === 'megaEvo' && this.gen >= 7) {
this.eachEvent('Update');
// In Gen 7, the decision order is recalculated for a Pokémon that mega evolves.
const moveIndex = this.queue.findIndex(queuedDecision => queuedDecision.pokemon === decision.pokemon && queuedDecision.choice === 'move');
if (moveIndex >= 0) {
const moveDecision = this.queue.splice(moveIndex, 1)[0];
this.insertQueue(moveDecision, true);
}
return false;
} else if (this.queue.length && this.queue[0].choice === 'instaswitch') {
return false;
}
let p1switch = this.p1.active.some(mon => mon && mon.switchFlag);
let p2switch = this.p2.active.some(mon => mon && mon.switchFlag);
if (p1switch && !this.canSwitch(this.p1)) {
for (let i = 0; i < this.p1.active.length; i++) {
this.p1.active[i].switchFlag = false;
}
p1switch = false;
}
if (p2switch && !this.canSwitch(this.p2)) {
for (let i = 0; i < this.p2.active.length; i++) {
this.p2.active[i].switchFlag = false;
}
p2switch = false;
}
if (p1switch || p2switch) {
if (this.gen >= 5) {
this.eachEvent('Update');
}
this.makeRequest('switch');
return true;
}
this.eachEvent('Update');
return false;
}
go() {
this.add('');
if (this.currentRequest) {
this.currentRequest = '';
}
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).
*/
changeDecision(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.
*/
choose(sideid, input) {
let side = null;
if (sideid === 'p1' || sideid === 'p2') side = this[sideid];
if (!side) throw new Error(`Invalid side ${sideid}`);
if (!side.choose(input)) return false;
this.checkDecisions();
return true;
}
commitDecisions() {
this.updateSpeed();
let oldQueue = this.queue;
this.queue = [];
let oldFlag = this.LEGACY_API_DO_NOT_USE;
this.LEGACY_API_DO_NOT_USE = false;
for (const side of this.sides) {
side.autoChoose();
}
this.LEGACY_API_DO_NOT_USE = oldFlag;
this.add('choice', this.p1.getChoice, this.p2.getChoice);
for (const side of this.sides) {
this.addQueue(side.choice.actions);
}
this.sortQueue();
Array.prototype.push.apply(this.queue, oldQueue);
this.currentRequest = '';
this.p1.currentRequest = '';
this.p2.currentRequest = '';
this.go();
}
undoChoice(sideid) {
let side = null;
if (sideid === 'p1' || sideid === 'p2') side = this[sideid];
if (!side) throw new Error(`Invalid side ${sideid}`);
if (!side.currentRequest) return;
if (side.choice.cantUndo) {
side.emitChoiceError(`Can't undo: A trapping/disabling effect would cause undo to leak information`);
return;
}
side.clearChoice();
}
/**
* @return true if both decisions are complete
*/
checkDecisions() {
let totalDecisions = 0;
if (this.p1.isChoiceDone()) {
if (!this.supportCancel) this.p1.choice.cantUndo = true;
totalDecisions++;
}
if (this.p2.isChoiceDone()) {
if (!this.supportCancel) this.p2.choice.cantUndo = true;
totalDecisions++;
}
if (totalDecisions >= this.sides.length) {
this.commitDecisions();
return true;
}
return false;
}
add(...parts) {
if (!parts.some(part => typeof part === 'function')) {
this.log.push(`|${parts.join('|')}`);
return;
}
this.log.push('|split');
let sides = [null, this.sides[0], this.sides[1], true];
for (let i = 0; i < sides.length; ++i) {
let sideUpdate = '|' + parts.map(part => {
if (typeof part !== 'function') return part;
return part(sides[i]);
}).join('|');
this.log.push(sideUpdate);
}
}
addMove(...args) {
this.lastMoveLine = this.log.length;
this.log.push(`|${args.join('|')}`);
}
attrLastMove(...args) {
this.log[this.lastMoveLine] += `|${args.join('|')}`;
}
retargetLastMove(newTarget) {
let parts = this.log[this.lastMoveLine].split('|');
parts[4] = newTarget;
this.log[this.lastMoveLine] = parts.join('|');
}
debug(activity) {
if (this.getFormat().debug) {
this.add('debug', activity);
}
}
debugError(activity) {
this.add('debug', activity);
}
// players
join(slot, name, avatar, team) {
if (this.p1 && this.p2) return false;
if ((this.p1 && this.p1.name === name) || (this.p2 && this.p2.name === name)) return false;
let player = null;
if (slot !== 'p1' && slot !== 'p2') slot = (this.p1 ? 'p2' : 'p1');
let slotNum = (slot === 'p2' ? 1 : 0);
if (this.started) {
this[slot].name = name;
} else {
//console.log("NEW SIDE: " + name);
this[slot] = new Sim.Side(name, this, slotNum, team);
this.sides[slotNum] = this[slot];
}
player = this[slot];
if (avatar) player.avatar = avatar;
this.add('player', player.id, player.name, avatar);
if (!this.started) this.add('teamsize', player.id, player.pokemon.length);
this.start();
return player;
}
// This function is called by this process's 'message' event.
receive(data, more) {
this.messageLog.push(data.join(' '));
let logPos = this.log.length;
let alreadyEnded = this.ended;
switch (data[1]) {
case 'join': {
let team;
if (more) team = Dex.fastUnpackTeam(more);
this.join(data[2], data[3], data[4], team);
break;
}
case 'win':
case 'tie':
this.win(data[2]);
break;
case 'choose':
this.choose(data[2], data[3]);
break;
case 'undo':
this.undoChoice(data[2]);
break;
case 'eval': {
/* eslint-disable no-eval, no-unused-vars */
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);
}
/* eslint-enable no-eval, no-unused-vars */
break;
}
default:
// unhandled
}
this.sendUpdates(logPos, alreadyEnded);
}
sendUpdates(logPos, alreadyEnded) {
if (this.log.length > logPos) {
if (alreadyEnded !== undefined && this.ended && !alreadyEnded) {
if (this.rated || Config.logchallenges) {
let log = {
seed: this.prngSeed,
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));
}
}
}
destroy() {
// 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;
}
}
module.exports = Battle;