/** * 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}; if (!require('./crashlogger.js')(fakeErr, 'A battle')) { var ministack = (""+err.stack).split("\n").slice(0,2).join("
"); process.send(data[0]+'\nupdate\n|html|
A BATTLE PROCESS HAS CRASHED: '+ministack+'
'); } else { process.send(data[0]+'\nupdate\n|html|
The battle crashed!
Don\'t worry, we\'re working on fixing it.
'); } } } } 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'); var logPos = battle.log.length; battle.add('html', '
The battle crashed
You can keep playing but it might crash again.
'); var nestedError; try { battle.makeRequest(prevRequest); } catch (e) { nestedError = e; } battle.sendUpdates(logPos); if (nestedError) { throw nestedError; } } } 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, user) { var template = pokemon.template; if (pokemon.fainted || pokemon.illusion || pokemon.volatiles['substitute']) { return false; } if (!template.abilities || (pokemon && pokemon.transformed && this.battle.gen >= 2) || (user && user.transformed && this.battle.gen >= 5)) { return false; } if (!this.formeChange(template, true)) { return false; } this.transformed = true; this.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); var hasRelayVar = true; if (relayVar === undefined) { relayVar = true; hasRelayVar = false; } if (target.fainted) { return false; } if (effect.effectType === 'Status' && target.status !== effect.id) { // it's changed; call it off return relayVar; } if (target.ignore && target.ignore[effect.effectType]) { this.debug(eventid+' handler suppressed by Klutz or Magic Room'); return relayVar; } if (target.ignore && target.ignore[effect.effectType+'Target']) { 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) { 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(); } else { // some kind of weird race condition? this.commitDecisions(); } 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.singleEvent('BasePower', move, null, pokemon, target, move, 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) { this.commitDecisions(); } }; Battle.prototype.commitDecisions = function() { 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 validMoves = pokemon.getValidMoves(); var moveid = ''; if (data.search(/^[0-9]+$/) >= 0) { moveid = validMoves[parseInt(data, 10) - 1]; } else { moveid = toId(data); if (moveid.substr(0, 11) === 'hiddenpower') { moveid = 'hiddenpower'; } if (validMoves.indexOf(moveid) < 0) { moveid = ''; } } if (!moveid) { moveid = validMoves[0]; } decisions.push({ choice: 'move', pokemon: pokemon, targetLoc: targetLoc, move: moveid }); 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; data[2] = data[2].replace(/\f/g, '\n'); this.add('', '>>> '+data[2]); try { this.add('', '<<< '+eval(data[2])); } 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) { 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