)
// ((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.
var a = [0x5D58, 0x8B65, 0x6C07, 0x8965];
var c = [0, 0, 0x26, 0x9EC3];
var nextSeed = [0, 0, 0, 0];
var carry = 0;
for (var cN = seed.length - 1; cN >= 0; --cN) {
nextSeed[cN] = carry;
carry = 0;
var aN = seed.length - 1;
var seedN = cN;
for (; seedN < seed.length; --aN, ++seedN) {
var 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) {
var 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) {
var oldstatus = this.getWeather();
this.singleEvent('End', oldstatus, this.weatherData, this);
}
var prevWeather = this.weather;
var 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) {
var 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) {
var oldstatus = this.getTerrain();
this.singleEvent('End', oldstatus, this.terrainData, this);
}
var prevTerrain = this.terrain;
var 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) {
var 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 () {
var pokemon;
for (var i = 0; i < this.sides.length; i++) {
for (var 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 () {
var actives = this.p1.active;
for (var i = 0; i < actives.length; i++) {
if (actives[i]) actives[i].update();
}
actives = this.p2.active;
for (var 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) {
var 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) {
var actives = [];
if (!effect && this.effect) effect = this.effect;
for (var i = 0; i < this.sides.length; i++) {
var side = this.sides[i];
for (var 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 (var i = 0; i < actives.length; i++) {
if (actives[i].isStarted) {
this.runEvent(eventid, actives[i], null, effect, relayVar);
}
}
};
Battle.prototype.residualEvent = function (eventid, relayVar) {
var statuses = this.getRelevantEffectsInner(this, 'on' + eventid, null, null, false, true, 'duration');
statuses.sort(Battle.comparePriority);
while (statuses.length) {
var statusObj = statuses.shift();
var 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);
var 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;
var parentEffect = this.effect;
var parentEffectData = this.effectData;
var parentEvent = this.event;
this.effect = effect;
this.effectData = effectData;
this.event = {id: eventid, target: target, source: source, effect: sourceEffect};
this.eventDepth++;
var args = [target, source, sourceEffect];
if (hasRelayVar) args.unshift(relayVar);
var 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;
var statuses = this.getRelevantEffects(target, 'on' + eventid, 'onSource' + eventid, source);
var hasRelayVar = true;
effect = this.getEffect(effect);
var 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);
}
var 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 (var i = 0; i < statuses.length; i++) {
var status = statuses[i].status;
var 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
var AttackingEvents = {
BeforeMove: 1,
BasePower: 1,
Immunity: 1,
Accuracy: 1,
RedirectTarget: 1,
Heal: 1,
SetStatus: 1,
CriticalHit: 1,
ModifyPokemon: 1,
ModifyAtk: 1, ModifyDef: 1, ModifySpA: 1, ModifySpD: 1, ModifySpe: 1,
ModifyBoost: 1,
ModifyDamage: 1,
ModifyWeight: 1,
TryHit: 1,
TryHitSide: 1,
TryMove: 1,
TrySecondaryHit: 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;
}
var returnVal;
if (typeof statuses[i].callback === 'function') {
var parentEffect = this.effect;
var 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) {
var order = false;
var priority = 0;
var subOrder = 0;
var 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) {
var 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 [];
var statuses = [];
var status;
if (thing.sides) {
for (var 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) {
var handler, statusData;
for (var i = 0; i < this.events[callbackType].length; i++) {
handler = this.events[callbackType][i];
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 (var 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') {
var 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 (var 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;
}
var 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 (var 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) {
var foeActive = thing.side.foe.active;
var allyActive = thing.side.active;
var 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 (var 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 (var 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 (var 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 TypeError("Event handlers must have an event to listen to");
if (!target) throw TypeError("Event handlers must have a target");
if (arguments.length < 3) throw TypeError("Event handlers must have a callback");
if (target.effectType !== 'Format') {
throw TypeError("" + target.effectType + " targets are not supported at this time");
}
var callback, priority, order, subOrder;
if (arguments.length === 3) {
callback = arguments[2];
priority = 0;
order = false;
subOrder = 0;
} else {
callback = arguments[3];
var 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;
}
}
var eventHandler = {callback: callback, target: target, priority: priority, order: order, subOrder: subOrder};
var 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 (var i = 0; i < this.p1.pokemon.length; i++) {
var pokemon = this.p1.pokemon[i];
if (pokemon.id === id) return pokemon;
}
for (var i = 0; i < this.p2.pokemon.length; i++) {
var 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
var p1request = null;
var p2request = null;
this.p1.currentRequest = '';
this.p2.currentRequest = '';
switch (type) {
case 'switch':
var switchTable = [];
var active;
for (var i = 0, l = this.p1.active.length; i < l; i++) {
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 (var i = 0, l = this.p2.active.length; i < l; i++) {
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:
var activeData;
this.p1.currentRequest = 'move';
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) {
var 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', 'The battle crashed
');
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;
var side = pokemon.side;
if (pos >= side.active.length) {
throw new Error("Invalid switch position");
}
if (side.active[pos]) {
var oldActive = side.active[pos];
var lastMove = null;
lastMove = this.getMove(oldActive.lastMove);
if (oldActive.switchCopyFlag === 'copyvolatile') {
delete oldActive.switchCopyFlag;
pokemon.copyVolatileFrom(oldActive);
}
}
pokemon.isActive = true;
this.runEvent('BeforeSwitchIn', pokemon);
if (side.active[pos]) {
var 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 (var 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) {
var canSwitchIn = [];
for (var i = side.active.length; i < side.pokemon.length; i++) {
var pokemon = side.pokemon[i];
if (!pokemon.fainted) {
canSwitchIn.push(pokemon);
}
}
return canSwitchIn.length;
};
Battle.prototype.getRandomSwitchable = function (side) {
var canSwitchIn = [];
for (var i = side.active.length; i < side.pokemon.length; i++) {
var 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;
var pokemon = this.getRandomSwitchable(side);
if (!pos) pos = 0;
if (!pokemon || pokemon.isActive) return false;
this.runEvent('BeforeSwitchIn', pokemon);
if (side.active[pos]) {
var 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 (var 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 (var 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");
}
var target = pokemon.side.active[slot];
if (slot !== 1 && (!target || target.fainted)) return false;
this.add('swap', pokemon, slot, attributes || '');
var 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++;
var allStale = true;
var oneStale = false;
for (var i = 0; i < this.sides.length; i++) {
for (var j = 0; j < this.sides[i].active.length; j++) {
var 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', '' + this.escapeHTML(pokemon.name) + ' isn\'t losing HP from Struggle. If this continues, it will be classified as being in an endless loop.
');
break;
case 'drag':
this.add('html', '' + 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.
');
break;
case 'switch':
this.add('html', '' + 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.
');
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', '' + 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.
');
}
}
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;
}
var banlistTable = this.getFormat().banlistTable;
if (banlistTable && 'Rule:endlessbattleclause' in banlistTable) {
if (oneStale) {
var activationWarning = '
If all active Pokémon go in an endless loop, Endless Battle Clause will activate.';
if (allStale) activationWarning = '';
var 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', '' + this.escapeHTML(oneStale.name) + ' is in an endless loop' + loopReason + '.' + activationWarning + '
');
oneStale.staleWarned = true;
this.firstStaleWarned = true;
}
if (allStale) {
this.add('message', "All active Pok\u00e9mon are in an endless loop. Endless Battle Clause activated!");
var leppaPokemon = null;
for (var i = 0; i < this.sides.length; i++) {
for (var j = 0; j < this.sides[i].pokemon.length; j++) {
var 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', 'If this format had Endless Battle Clause, it would have activated.
');
} else if (oneStale) {
this.add('html', '' + this.escapeHTML(oneStale.name) + ' is in an endless loop.
');
oneStale.staleWarned = true;
}
}
this.add('turn', this.turn);
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.
var center = false;
for (var i = 0; i < this.sides.length; i++) {
for (var 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.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);
var format = this.getFormat();
Tools.mod(format.mod).getBanlistTable(format); // fill in format ruleset
this.add('tier', format.name);
if (this.rated) {
this.add('rated');
}
if (format && format.ruleset) {
for (var 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));
var success = false;
for (var i in boost) {
var currentBoost = {};
currentBoost[i] = boost[i];
if (boost[i] !== 0 && target.boostBy(currentBoost)) {
success = true;
var msg = '-boost';
if (boost[i] < 0) {
msg = '-unboost';
boost[i] = -boost[i];
}
switch (effect.id) {
case 'bellydrum':
// No message
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 {
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;
var 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) {
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) {
var previousMod = Math.floor(this.event.modifier * 4096);
if (numerator.length) {
denominator = numerator[1];
numerator = numerator[0];
}
var 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];
}
var 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 = '???';
var type = move.type;
// '???' is typeless damage: used for Struggle and Confusion etc
var category = this.getCategory(move);
var defensiveCategory = move.defensiveCategory || category;
var 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);
var 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);
var level = pokemon.level;
var attacker = pokemon;
var defender = target;
var attackStat = category === 'Physical' ? 'atk' : 'spa';
var defenseStat = defensiveCategory === 'Physical' ? 'def' : 'spd';
var statTable = {atk:'Atk', def:'Def', spa:'SpA', spd:'SpD', spe:'Spe'};
var attack;
var defense;
var atkBoosts = move.useTargetOffensive ? defender.boosts[attackStat] : attacker.boosts[attackStat];
var defBoosts = move.useSourceDefensive ? attacker.boosts[defenseStat] : defender.boosts[defenseStat];
var ignoreNegativeOffensive = !!move.ignoreNegativeOffensive;
var ignorePositiveDefensive = !!move.ignorePositiveDefensive;
if (move.crit) {
ignoreNegativeOffensive = true;
ignorePositiveDefensive = true;
}
var ignoreOffensive = !!(move.ignoreOffensive || (ignoreNegativeOffensive && atkBoosts < 0));
var 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);
var baseDamage = Math.floor(Math.floor(Math.floor(2 * level / 5 + 2) * basePower * attack / defense) / 50) + 2;
// multi-target modifier (doubles only)
if (move.spreadHit) {
var spreadModifier = move.spreadModifier || 0.75;
this.debug('Spread modifier: ' + spreadModifier);
baseDamage = this.modify(baseDamage, spreadModifier);
}
// weather modifier (TODO: relocate here)
// crit
if (move.crit) {
if (!suppressMessages) this.add('-crit', target);
baseDamage = this.modify(baseDamage, move.critModifier || (this.gen >= 6 ? 1.5 : 2));
}
// randomizer
// this is not a modifier
baseDamage = Math.floor(baseDamage * (100 - this.random(16)) / 100);
// 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 (var i = 0; i < move.typeMod; i++) {
baseDamage *= 2;
}
}
if (move.typeMod < 0) {
if (!suppressMessages) this.add('-resisted', target);
for (var i = 0; i > move.typeMod; i--) {
baseDamage = Math.floor(baseDamage / 2);
}
}
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);
};
/**
* Returns whether a proposed target for a move is valid.
*/
Battle.prototype.validTargetLoc = function (targetLoc, source, targetType) {
var numSlots = source.side.active.length;
if (!Math.abs(targetLoc) && Math.abs(targetLoc) > numSlots) return false;
var sourceLoc = -(source.position + 1);
var isFoe = (targetLoc > 0);
var isAdjacent = (isFoe ? Math.abs(-(numSlots + 1 - targetLoc) - sourceLoc) <= 1 : Math.abs(targetLoc - sourceLoc) === 1);
var 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) {
var move = this.getMove(decision.move);
var 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') {
var 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') {
var foeActives = pokemon.side.foe.active;
var frontPosition = foeActives.length - 1 - pokemon.position;
var 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());
}
var 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 (var i = 0; i < this.p1.active.length; i++) {
this.cancelMove(this.p1.active[i]);
}
for (var 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) {
var 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) {
var 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) {
var 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 (var 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 (var i = 0; i < decision.length; i++) {
this.insertQueue(decision[i]);
}
return;
}
this.resolvePriority(decision);
for (var 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 (var 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 (var 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 (var 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) {
var success = false;
for (var 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 (var 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 (var 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) {
var pokemon;
// returns whether or not we ended in a callback
switch (decision.choice) {
case 'start':
// I GIVE UP, WILL WRESTLE WITH EVENT SYSTEM LATER
var beginCallback = this.getFormat().onBegin;
if (beginCallback) beginCallback.call(this);
this.add('start');
for (var pos = 0; pos < this.p1.active.length; pos++) {
this.switchIn(this.p1.pokemon[pos], pos);
}
for (var pos = 0; pos < this.p2.active.length; pos++) {
this.switchIn(this.p2.pokemon[pos], pos);
}
for (var pos = 0; pos < this.p1.pokemon.length; pos++) {
pokemon = this.p1.pokemon[pos];
this.singleEvent('Start', this.getEffect(pokemon.species), pokemon.speciesData, pokemon);
}
for (var pos = 0; pos < this.p2.pokemon.length; pos++) {
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);
var 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':
var len = decision.side.pokemon.length;
var newPokemon = [null, null, null, null, null, null].slice(0, len);
for (var j = 0; j < len; j++) {
var 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;
var lastMove = this.getMove(decision.pokemon.lastMove);
if (lastMove.selfSwitch !== 'copyvolatile') {
this.runEvent('BeforeSwitchOut', decision.pokemon);
}
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) {
var foeActive = decision.pokemon.side.foe.active;
for (var 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;
this.swapPosition(decision.pokemon, 1);
var foeActive = decision.pokemon.side.foe.active;
for (var 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)
var 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; }
var p1switch = this.p1.active.any(hasSwitchFlag);
var 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) {
var 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) {
var 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;
}
// It should be impossible for choice not to be a string. Choice comes
// from splitting the string sent by our forked process, not from the
// client. However, just in case, we maintain this check for now.
if (typeof choice === 'string') choice = choice.split(',');
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, side);
if (this.p1.decision && this.p2.decision) {
this.commitDecisions();
}
};
Battle.prototype.commitDecisions = function () {
var oldQueue = this.queue;
this.queue = [];
if (this.p1.decision !== true) {
this.addQueue(this.p1.resolveDecision());
}
if (this.p2.decision !== true) {
this.addQueue(this.p2.resolveDecision());
}
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) {
var 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) {
var prevSwitches = {};
if (!side.currentRequest) return true;
if (typeof choices === 'string') choices = choices.split(',');
var decisions = [];
var len = choices.length;
if (side.currentRequest !== 'teampreview') len = side.active.length;
var isDefault;
var choosableTargets = {normal:1, any:1, adjacentAlly:1, adjacentAllyOrSelf:1, adjacentFoe:1};
var freeSwitchCount = {'switch':0, 'pass':0};
if (side.currentRequest === 'switch') {
var 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 (var i = 0; i < len; i++) {
var choice = (choices[i] || '').trim();
var data = '';
var firstSpaceIndex = choice.indexOf(' ');
if (firstSpaceIndex >= 0) {
data = choice.substr(firstSpaceIndex + 1).trim();
choice = choice.substr(0, firstSpaceIndex).trim();
}
var 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;
}
var 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;
var moves = pokemon.getMoves();
var moveid = 'struggle';
for (var 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':
var pokemonLength = side.pokemon.length;
if (!data || data.length > pokemonLength) return false;
var dataArr = [0, 1, 2, 3, 4, 5].slice(0, pokemonLength);
var slotMap = dataArr.slice(); // Inverse of `dataArr` (slotMap[dataArr[x]] === x)
var tempSlot;
for (var j = 0; j < data.length; j++) {
var 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
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) {
var 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':
var moveid = '';
var targetLoc = 0;
var 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.
*/
var requestMoves = pokemon.getRequestData().moves;
if (data.search(/^[0-9]+$/) >= 0) {
// parse a one-based move index
var 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';
}
var isValidMove = false;
for (var 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.
*/
var 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.
var isEnabled = false;
for (var 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
var 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 () {
var parts = Array.prototype.slice.call(arguments);
var functions = parts.map(function (part) {
return typeof part === 'function';
});
if (functions.indexOf(true) < 0) {
this.log.push('|' + parts.join('|'));
} else {
this.log.push('|split');
var sides = [null, this.sides[0], this.sides[1], true];
for (var i = 0; i < sides.length; ++i) {
var line = '';
for (var 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') {
var 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') {
var 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(' '));
var logPos = this.log.length;
var alreadyEnded = this.ended;
switch (data[1]) {
case 'join':
var team = null;
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':
var battle = this;
var p1 = this.p1;
var p2 = this.p2;
var p1active = p1 ? p1.active[0] : null;
var p2active = p2 ? p2.active[0] : null;
var 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;
}
this.sendUpdates(logPos, alreadyEnded);
};
Battle.prototype.sendUpdates = function (logPos, alreadyEnded) {
if (this.p1 && this.p2) {
var 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) {
var 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 (var 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 (var 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;