/**
* Simulator process
* Pokemon Showdown - http://pokemonshowdown.com/
*
* This file is where the battle simulation itself happens.
*
* The most important part of the simulation happens in runEvent -
* see that function's definition for details.
*
* @license MIT license
*/
require('sugar');
fs = require('fs');
if (!('existsSync' in fs)) {
// for compatibility with ancient versions of node
fs.existsSync = require('path').existsSync;
}
config = require('./config/config.js');
if (config.crashguard) {
// graceful crash - allow current battles to finish before restarting
process.on('uncaughtException', function (err) {
require('./crashlogger.js')(err, 'A simulator process');
/* var stack = (""+err.stack).split("\n").slice(0,2).join("
");
Rooms.lobby.addRaw('
THE SERVER HAS CRASHED: '+stack+'
Please restart the server.
');
Rooms.lobby.addRaw('You will not be able to talk in the lobby or start new battles until the server restarts.
');
config.modchat = 'crash';
lockdown = true; */
});
}
/**
* Converts anything to an ID. An ID must have only lowercase alphanumeric
* characters.
* If a string is passed, it will be converted to lowercase and
* non-alphanumeric characters will be stripped.
* If an object with an ID is passed, its ID will be returned.
* Otherwise, an empty string will be returned.
*/
toId = function(text) {
if (text && text.id) text = text.id;
else if (text && text.userid) text = text.userid;
return string(text).toLowerCase().replace(/[^a-z0-9]+/g, '');
};
toUserid = toId;
/**
* Validates a username or Pokemon nickname
*/
var bannedNameStartChars = {'~':1, '&':1, '@':1, '%':1, '+':1, '-':1, '!':1, '?':1, '#':1, ' ':1};
toName = function(name) {
name = string(name);
name = name.replace(/[\|\s\[\]\,]+/g, ' ').trim();
while (bannedNameStartChars[name.charAt(0)]) {
name = name.substr(1);
}
if (name.length > 18) name = name.substr(0,18);
if (config.namefilter) {
name = config.namefilter(name);
}
return name;
};
/**
* Escapes a string for HTML
* If strEscape is true, escapes it for JavaScript, too
*/
sanitize = function(str, strEscape) {
str = (''+(str||''));
str = str.escapeHTML();
if (strEscape) str = str.replace(/'/g, '\\\'');
return str;
};
/**
* Safely ensures the passed variable is a string
* Simply doing ''+str can crash if str.toString crashes or isn't a function
* If we're expecting a string and being given anything that isn't a string
* or a number, it's safe to assume it's an error, and return ''
*/
string = function(str) {
if (typeof str === 'string' || typeof str === 'number') return ''+str;
return '';
}
/**
* Converts any variable to an integer (numbers get floored, non-numbers
* become 0). Then clamps it between min and (optionally) max.
*/
clampIntRange = function(num, min, max) {
if (typeof num !== 'number') num = 0;
num = Math.floor(num);
if (num < min) num = min;
if (typeof max !== 'undefined' && num > max) num = max;
return num;
};
Data = {};
Tools = require('./tools.js');
var Battles = {};
// Receive and process a message sent using Simulator.prototype.send in
// another process.
process.on('message', function(message) {
//console.log('CHILD MESSAGE RECV: "'+message+'"');
var nlIndex = message.indexOf("\n");
var more = '';
if (nlIndex > 0) {
more = message.substr(nlIndex+1);
message = message.substr(0, nlIndex);
}
var data = message.split('|');
if (data[1] === 'init') {
if (!Battles[data[0]]) {
try {
Battles[data[0]] = Battle.construct(data[0], data[2], data[3]);
} catch (err) {
var stack = err.stack + '\n\n' +
'Additional information:\n' +
'message = ' + message;
var fakeErr = {stack: stack};
require('./crashlogger.js')(fakeErr, 'A battle');
process.send(data[0]+'\nupdate\n|html|The battle crashed
');
}
}
} else if (data[1] === 'dealloc') {
if (Battles[data[0]]) Battles[data[0]].destroy();
delete Battles[data[0]];
} else {
var battle = Battles[data[0]];
if (battle) {
var prevRequest = battle.currentRequest;
try {
battle.receive(data, more);
} catch (err) {
var stack = err.stack + '\n\n' +
'Additional information:\n' +
'message = ' + message + '\n' +
'currentRequest = ' + prevRequest + '\n\n' +
'Log:\n' + battle.log.join('\n');
var fakeErr = {stack: stack};
require('./crashlogger.js')(fakeErr, 'A battle');
battle.add('html', 'The battle crashed
You can keep playing but it might crash again.
');
battle.makeRequest(prevRequest);
battle.sendUpdates();
}
} else if (data[1] === 'eval') {
try {
eval(data[2]);
} catch (e) {}
}
}
});
var BattlePokemon = (function() {
function BattlePokemon(set, side) {
this.side = side;
this.battle = side.battle;
if (typeof set === 'string') set = {name: set};
// "pre-bound" functions for nicer syntax (avoids repeated use of `bind`)
this.getHealth = BattlePokemon.getHealth.bind(this);
this.getDetails = BattlePokemon.getDetails.bind(this);
this.set = set;
this.baseTemplate = this.battle.getTemplate(set.species || set.name);
if (!this.baseTemplate.exists) {
this.battle.debug('Unidentified species: '+this.species);
this.baseTemplate = this.battle.getTemplate('Bulbasaur');
}
this.species = this.baseTemplate.species;
if (set.name === set.species || !set.name || !set.species) {
set.name = this.species;
}
this.name = (set.name || set.species || 'Bulbasaur').substr(0,20);
this.speciesid = toId(this.species);
this.template = this.baseTemplate;
this.moves = [];
this.baseMoves = this.moves;
this.movepp = {};
this.moveset = [];
this.baseMoveset = [];
this.level = clampIntRange(set.forcedLevel || set.level || 100, 1, 1000);
var genders = {M:'M',F:'F'};
this.gender = this.template.gender || genders[set.gender] || (Math.random()*2<1?'M':'F');
if (this.gender === 'N') this.gender = '';
this.happiness = typeof set.happiness === 'number' ? clampIntRange(set.happiness, 0, 255) : 255;
this.fullname = this.side.id + ': ' + this.name;
this.details = this.species + (this.level==100?'':', L'+this.level) + (this.gender===''?'':', '+this.gender) + (this.set.shiny?', shiny':'');
this.id = this.fullname; // shouldn't really be used anywhere
this.statusData = {};
this.volatiles = {};
this.negateImmunity = {};
this.height = this.template.height;
this.heightm = this.template.heightm;
this.weight = this.template.weight;
this.weightkg = this.template.weightkg;
this.ignore = {};
this.baseAbility = toId(set.ability);
this.ability = this.baseAbility;
this.item = toId(set.item);
this.abilityData = {id: this.ability};
this.itemData = {id: this.item};
this.speciesData = {id: this.speciesid};
this.types = this.baseTemplate.types;
if (this.set.moves) {
for (var i=0; i 6) boost = 6;
if (boost < -6) boost = -6;
if (boost >= 0) {
stat = Math.floor(stat * boostTable[boost]);
} else {
stat = Math.floor(stat / boostTable[-boost]);
}
if (this.battle.getStatCallback) {
stat = this.battle.getStatCallback(stat, statName, this);
}
return stat;
};
BattlePokemon.prototype.getMoveData = function(move) {
move = this.battle.getMove(move);
for (var i=0; i 0) boosts += this.boosts[i];
}
return boosts;
};
BattlePokemon.prototype.boostBy = function(boost, source, effect) {
var changed = false;
for (var i in boost) {
var delta = boost[i];
this.boosts[i] += delta;
if (this.boosts[i] > 6) {
delta -= this.boosts[i] - 6;
this.boosts[i] = 6;
}
if (this.boosts[i] < -6) {
delta -= this.boosts[i] - (-6);
this.boosts[i] = -6;
}
if (delta) changed = true;
}
this.update();
return changed;
};
BattlePokemon.prototype.clearBoosts = function() {
for (var i in this.boosts) {
this.boosts[i] = 0;
}
this.update();
};
BattlePokemon.prototype.setBoost = function(boost) {
for (var i in boost) {
this.boosts[i] = boost[i];
}
this.update();
};
BattlePokemon.prototype.copyVolatileFrom = function(pokemon) {
this.clearVolatile();
this.boosts = pokemon.boosts;
this.volatiles = pokemon.volatiles;
this.update();
pokemon.clearVolatile();
for (var i in this.volatiles) {
var status = this.getVolatile(i);
if (status.noCopy) {
delete this.volatiles[i];
}
this.battle.singleEvent('Copy', status, this.volatiles[i], this);
}
};
BattlePokemon.prototype.transformInto = function(pokemon) {
var template = pokemon.template;
if (pokemon.fainted || pokemon.illusion || pokemon.volatiles['substitute']) {
return false;
}
if (!template.abilities || pokemon && pokemon.transformed) {
return false;
}
if (!this.formeChange(template, true)) {
return false;
}
this.transformed = true;
this.types = pokemon.types;
for (var statName in this.stats) {
this.stats[statName] = pokemon.stats[statName];
}
this.ability = pokemon.ability;
this.moveset = [];
this.moves = [];
for (var i=0; i 0) d = 1;
d = Math.floor(d);
if (isNaN(d)) return 0;
if (d <= 0) return 0;
this.hp -= d;
if (this.hp <= 0) {
d += this.hp;
this.faint(source, effect);
}
return d;
};
BattlePokemon.prototype.hasMove = function(moveid) {
moveid = toId(moveid);
if (moveid.substr(0,11) === 'hiddenpower') moveid = 'hiddenpower';
for (var i=0; i= this.maxhp) return 0;
this.hp += d;
if (this.hp > this.maxhp) {
d -= this.hp - this.maxhp;
this.hp = this.maxhp;
}
return d;
};
// sets HP, returns delta
BattlePokemon.prototype.sethp = function(d) {
if (!this.hp) return 0;
d = Math.floor(d);
if (isNaN(d)) return;
if (d < 1) d = 1;
d = d-this.hp;
this.hp += d;
if (this.hp > this.maxhp) {
d -= this.hp - this.maxhp;
this.hp = this.maxhp;
}
return d;
};
BattlePokemon.prototype.trySetStatus = function(status, source, sourceEffect) {
if (!this.hp) return false;
if (this.status) return false;
return this.setStatus(status, source, sourceEffect);
};
BattlePokemon.prototype.cureStatus = function() {
if (!this.hp) return false;
// unlike clearStatus, gives cure message
if (this.status) {
this.battle.add('-curestatus', this, this.status);
this.setStatus('');
}
};
BattlePokemon.prototype.setStatus = function(status, source, sourceEffect, ignoreImmunities) {
if (!this.hp) return false;
status = this.battle.getEffect(status);
if (this.battle.event) {
if (!source) source = this.battle.event.source;
if (!sourceEffect) sourceEffect = this.battle.effect;
}
if (!ignoreImmunities && status.id) {
// the game currently never ignores immunities
if (!this.runImmunity(status.id==='tox'?'psn':status.id)) {
this.battle.debug('immune to status');
return false;
}
}
if (this.status === status.id) return false;
var prevStatus = this.status;
var prevStatusData = this.statusData;
if (status.id && !this.battle.runEvent('SetStatus', this, source, sourceEffect, status)) {
this.battle.debug('set status ['+status.id+'] interrupted');
return false;
}
this.status = status.id;
this.statusData = {id: status.id, target: this};
if (source) this.statusData.source = source;
if (status.duration) {
this.statusData.duration = status.duration;
}
if (status.durationCallback) {
this.statusData.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
}
if (status.id && !this.battle.singleEvent('Start', status, this.statusData, this, source, sourceEffect)) {
this.battle.debug('status start ['+status.id+'] interrupted');
// cancel the setstatus
this.status = prevStatus;
this.statusData = prevStatusData;
return false;
}
this.update();
if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
return false;
}
return true;
};
BattlePokemon.prototype.clearStatus = function() {
// unlike cureStatus, does not give cure message
return this.setStatus('');
};
BattlePokemon.prototype.getStatus = function() {
return this.battle.getEffect(this.status);
};
BattlePokemon.prototype.eatItem = function(item, source, sourceEffect) {
if (!this.hp || !this.isActive) return false;
if (!this.item) return false;
var id = toId(item);
if (id && this.item !== id) return false;
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
item = this.getItem();
if (this.battle.runEvent('UseItem', this, null, null, item) && this.battle.runEvent('EatItem', this, null, null, item)) {
this.battle.add('-enditem', this, item, '[eat]');
this.battle.singleEvent('Eat', item, this.itemData, this, source, sourceEffect);
this.lastItem = this.item;
this.item = '';
this.itemData = {id: '', target: this};
this.usedItemThisTurn = true;
return true;
}
return false;
};
BattlePokemon.prototype.useItem = function(item, source, sourceEffect) {
if (!this.isActive) return false;
if (!this.item) return false;
var id = toId(item);
if (id && this.item !== id) return false;
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
item = this.getItem();
if (this.battle.runEvent('UseItem', this, null, null, item)) {
switch (item.id) {
case 'redcard':
this.battle.add('-enditem', this, item, '[of] '+source);
break;
default:
if (!item.isGem) {
this.battle.add('-enditem', this, item);
}
break;
}
this.battle.singleEvent('Use', item, this.itemData, this, source, sourceEffect);
this.lastItem = this.item;
this.item = '';
this.itemData = {id: '', target: this};
this.usedItemThisTurn = true;
return true;
}
return false;
};
BattlePokemon.prototype.takeItem = function(source) {
if (!this.hp || !this.isActive) return false;
if (!this.item) return false;
if (!source) source = this;
var item = this.getItem();
if (this.battle.runEvent('TakeItem', this, source, null, item)) {
this.lastItem = '';
this.item = '';
this.itemData = {id: '', target: this};
return item;
}
return false;
};
BattlePokemon.prototype.setItem = function(item, source, effect) {
if (!this.hp || !this.isActive) return false;
item = this.battle.getItem(item);
this.lastItem = this.item;
this.item = item.id;
this.itemData = {id: item.id, target: this};
if (item.id) {
this.battle.singleEvent('Start', item, this.itemData, this, source, effect);
}
if (this.lastItem) this.usedItemThisTurn = true;
return true;
};
BattlePokemon.prototype.getItem = function() {
return this.battle.getItem(this.item);
};
BattlePokemon.prototype.clearItem = function() {
return this.setItem('');
};
BattlePokemon.prototype.setAbility = function(ability, source, effect) {
if (!this.hp) return false;
ability = this.battle.getAbility(ability);
if (this.ability === ability.id) {
return false;
}
if (ability.id === 'Multitype' || ability.id === 'Illusion' || this.ability === 'Multitype') {
return false;
}
this.ability = ability.id;
this.abilityData = {id: ability.id, target: this};
if (ability.id) {
this.battle.singleEvent('Start', ability, this.abilityData, this, source, effect);
}
return true;
};
BattlePokemon.prototype.getAbility = function() {
return this.battle.getAbility(this.ability);
};
BattlePokemon.prototype.clearAbility = function() {
return this.setAbility('');
};
BattlePokemon.prototype.getNature = function() {
return this.battle.getNature(this.set.nature);
};
BattlePokemon.prototype.addVolatile = function(status, source, sourceEffect) {
if (!this.hp) return false;
status = this.battle.getEffect(status);
if (this.battle.event) {
if (!source) source = this.battle.event.source;
if (!sourceEffect) sourceEffect = this.battle.effect;
}
if (this.volatiles[status.id]) {
this.battle.singleEvent('Restart', status, this.volatiles[status.id], this, source, sourceEffect);
return false;
}
if (!this.runImmunity(status.id)) return false;
var result = this.battle.runEvent('TryAddVolatile', this, source, sourceEffect, status);
if (!result) {
this.battle.debug('add volatile ['+status.id+'] interrupted');
return result;
}
this.volatiles[status.id] = {id: status.id};
this.volatiles[status.id].target = this;
if (source) {
this.volatiles[status.id].source = source;
this.volatiles[status.id].sourcePosition = source.position;
}
if (sourceEffect) {
this.volatiles[status.id].sourceEffect = sourceEffect;
}
if (status.duration) {
this.volatiles[status.id].duration = status.duration;
}
if (status.durationCallback) {
this.volatiles[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
}
if (!this.battle.singleEvent('Start', status, this.volatiles[status.id], this, source, sourceEffect)) {
// cancel
delete this.volatiles[status.id];
return false;
}
this.update();
return true;
};
BattlePokemon.prototype.getVolatile = function(status) {
status = this.battle.getEffect(status);
if (!this.volatiles[status.id]) return null;
return status;
};
BattlePokemon.prototype.removeVolatile = function(status) {
if (!this.hp) return false;
status = this.battle.getEffect(status);
if (!this.volatiles[status.id]) return false;
this.battle.singleEvent('End', status, this.volatiles[status.id], this);
delete this.volatiles[status.id];
this.update();
return true;
};
// "static" function
BattlePokemon.getHealth = function(side) {
if (!this.hp) return '0 fnt';
var hpstring;
if ((side === true) || (this.side === side) || this.battle.getFormat().debug) {
hpstring = ''+this.hp+'/'+this.maxhp;
} else {
var ratio = this.hp / this.maxhp;
if (this.battle.reportPercentages) {
// HP Percentage Mod mechanics
var percentage = Math.ceil(ratio * 100);
if ((percentage === 100) && (ratio < 1.0)) {
percentage = 99;
}
hpstring = '' + percentage + '/100';
} else {
// In-game accurate pixel health mechanics
var pixels = Math.floor(ratio * 48) || 1;
hpstring = '' + pixels + '/48';
if ((pixels === 9) && (ratio > 0.2)) {
hpstring += 'y'; // force yellow HP bar
} else if ((pixels === 24) && (ratio > 0.5)) {
hpstring += 'g'; // force green HP bar
}
}
}
if (this.status) hpstring += ' ' + this.status;
return hpstring;
};
BattlePokemon.prototype.runImmunity = function(type, message) {
if (this.fainted) {
return false;
}
if (!type || type === '???') {
return true;
}
if (this.negateImmunity[type]) return true;
if (!this.negateImmunity['Type'] && !this.battle.getImmunity(type, this)) {
this.battle.debug('natural immunity');
if (message) {
this.battle.add('-immune', this, '[msg]');
}
return false;
}
var immunity = this.battle.runEvent('Immunity', this, null, null, type);
if (!immunity) {
this.battle.debug('artificial immunity');
if (message && immunity !== null) {
this.battle.add('-immune', this, '[msg]');
}
return false;
}
return true;
};
BattlePokemon.prototype.destroy = function() {
// deallocate ourself
// get rid of some possibly-circular references
this.battle = null;
this.side = null;
};
return BattlePokemon;
})();
var BattleSide = (function() {
function BattleSide(name, battle, n, team) {
this.battle = battle;
this.n = n;
this.name = name;
this.pokemon = [];
this.active = [null];
this.sideConditions = {};
this.id = (n?'p2':'p1');
switch (this.battle.gameType) {
case 'doubles':
this.active = [null, null];
break;
}
this.team = this.battle.getTeam(this, team);
for (var i=0; i>> 0; // truncate the result to the last 32 bits
var result = this.seed >>> 16; // the first 16 bits of the seed are the random value
m = Math.floor(m);
n = Math.floor(n);
return (m ? (n ? (result%(n-m))+m : result%m) : result/0x10000);
};
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) return false;
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.runEvent('TryWeather', target)) 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.getFormat = function() {
return this.getEffect(this.format);
};
Battle.prototype.addPseudoWeather = function(status, source, sourceEffect) {
status = this.getEffect(status);
if (this.pseudoWeather[status.id]) {
this.singleEvent('Restart', status, this.pseudoWeather[status.id], this, source, sourceEffect);
return false;
}
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.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= 5) {
// 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);
return false;
}
//this.add('Event: '+eventid+' (depth '+this.eventDepth+')');
effect = this.getEffect(effect);
if (target.fainted) {
return false;
}
if (effect.effectType === 'Status' && target.status !== effect.id) {
// it's changed; call it off
return true;
}
if (target.ignore && target.ignore[effect.effectType]) {
this.debug(eventid+' handler suppressed by Klutz or Magic Room');
return true;
}
if (target.ignore && target.ignore[effect.effectType+'Target']) {
this.debug(eventid+' handler suppressed by Air Lock');
return true;
}
if (typeof effect['on'+eventid] === 'undefined') return true;
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 (typeof relayVar !== 'undefined') args.unshift(relayVar);
var returnVal = true;
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 (typeof returnVal === 'undefined') return true;
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) {
if (this.eventDepth >= 5) {
// 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);
return false;
}
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 (typeof relayVar === 'undefined' || relayVar === null) {
relayVar = true;
hasRelayVar = false;
} else {
args.unshift(relayVar);
}
for (var i=0; iThe battle crashed');
this.win();
return;
}
this.add('callback', 'decision');
};
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 = '';
return true;
};
Battle.prototype.switchIn = function(pokemon, pos) {
if (!pokemon || pokemon.isActive) return false;
if (!pos) pos = 0;
var side = pokemon.side;
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);
}
}
this.runEvent('BeforeSwitchIn', pokemon);
if (side.active[pos]) {
var oldActive = side.active[pos];
oldActive.isActive = false;
oldActive.isStarted = false;
oldActive.position = pokemon.position;
pokemon.position = pos;
side.pokemon[pokemon.position] = pokemon;
side.pokemon[oldActive.position] = oldActive;
oldActive.clearVolatile();
}
side.active[pos] = pokemon;
pokemon.isActive = true;
pokemon.activeTurns = 0;
for (var m in pokemon.moveset) {
pokemon.moveset[m].used = false;
}
this.add('switch', side.active[pos], side.active[pos].getDetails);
pokemon.update();
this.runEvent('SwitchIn', pokemon);
this.addQueue({pokemon: pokemon, choice: 'runSwitch'});
};
Battle.prototype.canSwitch = function(side) {
var canSwitchIn = [];
for (var i=side.active.length; i= 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.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'
};
if (move.affectedByImmunities) {
if (!target.runImmunity(move.type, true)) {
return false;
}
}
if (move.isSoundBased && (pokemon !== target || this.gen <= 4)) {
if (!target.runImmunity('sound', true)) {
return false;
}
}
if (move.ohko) {
if (target.level > pokemon.level) {
this.add('-failed', target);
return false;
}
return target.maxhp;
}
if (!move.basePowerMultiplier && move.category !== 'Status') {
// happens before basePowerCallback so Acrobatics works correctly
// activates constant damage moves
// but NOT OHKO moves
move.basePowerMultiplier = this.runEvent('BasePowerMultiplier', pokemon, target, move, 1);
if (move.basePowerMultiplier != 1) this.debug('multiplier: '+move.basePowerMultiplier);
}
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 = clampIntRange(basePower, 1);
move.critRatio = clampIntRange(move.critRatio, 0, 5);
var critMult = [0, 16, 8, 4, 3, 2];
move.crit = move.willCrit || false;
if (typeof 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
if (basePower) {
basePower = this.runEvent('BasePower', pokemon, target, move, basePower);
if (move.basePowerMultiplier && move.basePowerMultiplier != 1) {
basePower = this.modify(basePower, move.basePowerMultiplier);
}
if (move.basePowerModifier) {
basePower = this.modify(basePower, move.basePowerModifier);
}
}
if (!basePower) return 0;
basePower = clampIntRange(basePower, 1);
var level = pokemon.level;
var attacker = pokemon;
var defender = target;
if (move.useTargetOffensive) attacker = target;
if (move.useSourceDefensive) defender = pokemon;
var attack = attacker.getStat(category==='Physical'?'atk':'spa');
var defense = defender.getStat(defensiveCategory==='Physical'?'def':'spd');
if (move.crit) {
move.ignoreNegativeOffensive = true;
move.ignorePositiveDefensive = true;
}
if (move.ignoreNegativeOffensive && attack < attacker.getStat(category==='Physical'?'atk':'spa', true)) {
move.ignoreOffensive = true;
}
if (move.ignoreOffensive) {
this.debug('Negating (sp)atk boost/penalty.');
attack = attacker.getStat(category==='Physical'?'atk':'spa', true);
}
if (move.ignorePositiveDefensive && defense > target.getStat(defensiveCategory==='Physical'?'def':'spd', true)) {
move.ignoreDefensive = true;
}
if (move.ignoreDefensive) {
this.debug('Negating (sp)def boost/penalty.');
defense = target.getStat(defensiveCategory==='Physical'?'def':'spd', true);
}
//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 || 2);
}
// randomizer
// this is not a modifier
// gen 1-2
//var randFactor = Math.floor(Math.random()*39)+217;
//baseDamage *= Math.floor(randFactor * 100 / 255) / 100;
baseDamage = Math.floor(baseDamage * (100 - this.random(16)) / 100);
// STAB
if (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
var totalTypeMod = this.getEffectiveness(type, target);
if (totalTypeMod > 0) {
if (!suppressMessages) this.add('-supereffective', target);
baseDamage *= 2;
if (totalTypeMod >= 2) {
baseDamage *= 2;
}
}
if (totalTypeMod < 0) {
if (!suppressMessages) this.add('-resisted', target);
baseDamage = Math.floor(baseDamage/2);
if (totalTypeMod <= -2) {
baseDamage = Math.floor(baseDamage/2);
}
}
if (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 && !isSelf;
case 'adjacentAlly':
return isAdjacent && !isSelf && !isFoe;
case 'adjacentAllyOrSelf':
return isAdjacent && !isFoe;
case 'adjacentFoe':
return isAdjacent && isFoe;
case 'any':
return !isSelf;
}
return false;
};
Battle.prototype.validTarget = function(target, source, targetType) {
var targetLoc;
if (target.side == source.side) {
targetLoc = -(target.position+1);
} else {
targetLoc = target.position+1;
}
return this.validTargetLoc(targetLoc, 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 && !target.fainted) return target;
}
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) {
move = this.getMove(move);
if (move.target === 'adjacentAlly' && pokemon.side.active.length > 1) {
if (pokemon.side.active[pokemon.position-1]) {
return pokemon.side.active[pokemon.position-1];
}
else if (pokemon.side.active[pokemon.position+1]) {
return pokemon.side.active[pokemon.position+1];
}
}
if (move.target === 'self' || move.target === 'all' || move.target === 'allySide' || move.target === 'allyTeam' || move.target === 'adjacentAlly' || move.target === 'adjacentAllyOrSelf') {
return pokemon;
}
return pokemon.side.foe.randomActive() || pokemon.side.foe.active[0];
};
Battle.prototype.checkFainted = function() {
function isFainted(a) {
if (!a) return false;
if (a.fainted) {
a.switchFlag = true;
return true;
}
return false;
}
// make sure these don't get short-circuited out; all switch flags need to be set
var p1fainted = this.p1.active.map(isFainted);
var p2fainted = this.p2.active.map(isFainted);
};
Battle.prototype.faintMessages = function() {
while (this.faintQueue.length) {
var faintData = this.faintQueue.shift();
if (!faintData.target.fainted) {
this.add('faint', faintData.target);
this.runEvent('Faint', faintData.target, faintData.source, faintData.effect);
faintData.target.fainted = true;
faintData.target.isActive = false;
faintData.target.isStarted = false;
faintData.target.side.pokemonLeft--;
faintData.target.side.faintedThisTurn = true;
}
}
if (!this.p1.pokemonLeft && !this.p2.pokemonLeft) {
this.win();
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.addQueue = function(decision, noSort, side) {
if (decision) {
if (Array.isArray(decision)) {
for (var i=0; i= 6 || i < 0) return;
if (decision.team[1]) {
// validate the choice
var len = decision.side.pokemon.length;
var newPokemon = [null,null,null,null,null,null].slice(0, len);
for (var j=0; j -6) {
this.eachEvent('Priority', null, currentPriority);
currentPriority--;
} */
this.runDecision(decision);
if (this.currentRequest) {
return;
}
// if (!this.queue.length || this.queue[0].choice === 'runSwitch') {
// if (this.faintMessages()) return;
// }
if (this.ended) return;
}
this.nextTurn();
this.midTurn = false;
this.queue = [];
};
/**
* Changes a pokemon's decision.
*
* The un-modded game should not use this function for anything,
* since it rerolls speed ties (which messes up RNG state).
*
* You probably 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.addQueue(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(',');
side.decision = this.parseChoice(choice, side);
if (this.p1.decision && this.p2.decision) {
if (this.p1.decision !== true) {
this.addQueue(this.p1.decision, true, this.p1);
}
if (this.p2.decision !== true) {
this.addQueue(this.p2.decision, true, this.p2);
}
this.currentRequest = '';
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");
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 === 'move') len = side.active.length;
for (var i=0; i= 0) {
data = choice.substr(firstSpaceIndex+1).trim();
choice = choice.substr(0, firstSpaceIndex).trim();
}
switch (side.currentRequest) {
case 'teampreview':
if (choice !== 'team' || i > 0) return false;
break;
case 'move':
if (i >= side.active.length) return false;
if (!side.pokemon[i] || side.pokemon[i].fainted) {
decisions.push({
choice: 'pass'
});
continue;
}
if (choice !== 'move' && choice !== 'switch') {
if (i === 0) return false;
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'
});
continue;
}
if (choice !== 'switch') return false;
break;
default:
return false;
}
var decision = null;
switch (choice) {
case 'team':
decisions.push({
choice: 'team',
side: side,
team: data
});
break;
case 'switch':
if (i > side.active.length || i > side.pokemon.length) continue;
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;
for (var j = i + 1; j < side.active.length; ++j) {
if (side.active[j] && !side.active[j].fainted) {
finalDecision = false;
}
}
decisions.finalDecision = decisions.finalDecision || finalDecision;
}
}
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 (this.battleType !== 'triples' && data < side.active.length) {
this.debug("Can't switch: You can't switch to an active pokemon except in triples");
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;
decisions.push({
choice: 'switch',
pokemon: side.pokemon[i],
target: side.pokemon[data]
});
break;
case 'move':
var targetLoc = 0;
if (data.substr(data.length-2) === ' 1') targetLoc = 1;
if (data.substr(data.length-2) === ' 2') targetLoc = 2;
if (data.substr(data.length-2) === ' 3') targetLoc = 3;
if (data.substr(data.length-3) === ' -1') targetLoc = -1;
if (data.substr(data.length-3) === ' -2') targetLoc = -2;
if (data.substr(data.length-3) === ' -3') targetLoc = -3;
if (targetLoc) data = data.substr(0, data.lastIndexOf(' '));
var pokemon = side.pokemon[i];
var move = '';
if (data.search(/^[0-9]+$/) >= 0) {
move = pokemon.getValidMoves()[parseInt(data,10)-1];
} else {
move = data;
}
if (!pokemon.canUseMove(move)) move = pokemon.getValidMoves()[0];
move = this.getMove(move).id;
decisions.push({
choice: 'move',
pokemon: pokemon,
targetLoc: targetLoc,
move: move
});
break;
}
}
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 = this.sides.concat(null, 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
// Simulator.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 = JSON.parse(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;
try {
this.add('chat', '~', '<<< '+eval(data[2]));
} catch (e) {
this.add('chatmsg', '<<< error: '+e.message);
}
break;
}
this.sendUpdates();
};
Battle.prototype.sendUpdates = function() {
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 (this.ended && !alreadyEnded) {
if (this.rated) {
var log = {
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('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