require('sugar'); fs = require('fs'); config = require('./config/config.js'); if (config.crashguard) { // graceful crash - allow current battles to finish before restarting process.on('uncaughtException', function (err) { console.log("\n"+err.stack+"\n"); fs.createWriteStream('logs/errors.txt', {'flags': 'a'}).on("open", function(fd) { this.write("\n"+err.stack+"\n"); this.end(); }).on("error", function (err) { console.log("\n"+err.stack+"\n"); }); /* 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}; toName = function(name) { name = string(name).trim(); name = name.replace(/(\||\n|\[|\]|\,)/g, ''); while (bannedNameStartChars[name.substr(0,1)]) { name = name.substr(1); } if (name.length > 18) name = name.substr(0,18); 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]]) { Battles[data[0]] = new Battle(data[0], data[2], data[3]); } } else if (data[1] === 'dealloc') { if (Battles[data[0]]) Battles[data[0]].destroy(); delete Battles[data[0]]; } else { if (Battles[data[0]]) { Battles[data[0]].receive(data, more); } else if (data[1] === 'eval') { try { eval(data[2]); } catch (e) {} } } }); function BattlePokemon(set, side) { var selfB = side.battle; var selfS = side; var selfP = this; this.side = side; if (typeof set === 'string') set = {name: set}; this.baseSet = set; this.set = this.baseSet; this.baseTemplate = selfB.getTemplate(set.species || set.name); if (!this.baseTemplate.exists) { selfB.debug('Unidentified species: '+this.species); this.baseTemplate = selfB.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.trapped = false; this.level = clampIntRange(set.forcedLevel || set.level || 100, 1, 100); this.hp = 0; this.maxhp = 100; 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.illusion = null; this.fainted = false; this.lastItem = ''; this.status = ''; this.statusData = {}; this.volatiles = {}; this.position = 0; this.lastMove = ''; this.lastDamage = 0; this.lastAttackedBy = null; this.movedThisTurn = false; this.usedItemThisTurn = false; this.newlySwitched = false; this.beingCalledBack = false; this.isActive = false; this.isStarted = false; // has this pokemon's Start events run yet? this.transformed = false; 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.duringMove = false; 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.hpType = 'Dark'; this.hpPower = 70; 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]); } return stat; }; this.getMoveData = function(move) { move = selfB.getMove(move); for (var i=0; i 0) boosts += selfP.boosts[i]; } return boosts; }; this.boostBy = function(boost, source, effect) { var changed = false; for (var i in boost) { var delta = boost[i]; selfP.baseBoosts[i] += delta; if (selfP.baseBoosts[i] > 6) { delta -= selfP.baseBoosts[i] - 6; selfP.baseBoosts[i] = 6; } if (selfP.baseBoosts[i] < -6) { delta -= selfP.baseBoosts[i] - (-6); selfP.baseBoosts[i] = -6; } if (delta) changed = true; } selfP.update(); return changed; }; this.clearBoosts = function() { for (var i in selfP.baseBoosts) { selfP.baseBoosts[i] = 0; } selfP.update(); }; this.setBoost = function(boost) { for (var i in boost) { selfP.baseBoosts[i] = boost[i]; } selfP.update(); }; this.copyVolatileFrom = function(pokemon) { selfP.clearVolatile(); selfP.baseBoosts = pokemon.baseBoosts; selfP.volatiles = pokemon.volatiles; selfP.update(); pokemon.clearVolatile(); for (var i in selfP.volatiles) { var status = selfP.getVolatile(i); if (status.noCopy) { delete selfP.volatiles[i]; } } }; this.transformInto = function(baseTemplate) { var pokemon = null; if (baseTemplate && baseTemplate.template) { pokemon = baseTemplate; baseTemplate = pokemon.template; if (pokemon.fainted || pokemon.illusion || pokemon.volatiles['substitute']) { return false; } } else if (!baseTemplate || !baseTemplate.abilities) { baseTemplate = selfB.getTemplate(baseTemplate); } if (!baseTemplate.abilities || pokemon && pokemon.transformed) { return false; } selfP.transformed = true; selfP.template = baseTemplate; selfP.baseStats = selfP.template.baseStats; selfP.types = baseTemplate.types; if (pokemon) { selfP.ability = pokemon.ability; selfP.set = pokemon.set; selfP.moveset = []; selfP.moves = []; for (var i=0; i 0) d = 1; d = Math.floor(d); if (isNaN(d)) return 0; if (d <= 0) return 0; selfP.hp -= d; if (selfP.hp <= 0) { d += selfP.hp; selfP.faint(source, effect); } return d; }; this.hasMove = function(moveid) { moveid = toId(moveid); if (moveid.substr(0,11) === 'hiddenpower') moveid = 'hiddenpower'; for (var i=0; i= selfP.maxhp) return 0; selfP.hp += d; if (selfP.hp > selfP.maxhp) { d -= selfP.hp - selfP.maxhp; selfP.hp = selfP.maxhp; } return d; }; // sets HP, returns delta this.sethp = function(d) { if (!selfP.hp) return 0; d = Math.floor(d); if (isNaN(d)) return; if (d < 1) d = 1; d = d-selfP.hp; selfP.hp += d; if (selfP.hp > selfP.maxhp) { d -= selfP.hp - selfP.maxhp; selfP.hp = selfP.maxhp; } return d; }; this.trySetStatus = function(status, source, sourceEffect) { if (!selfP.hp) return false; if (selfP.status) return false; return selfP.setStatus(status, source, sourceEffect); }; this.cureStatus = function() { if (!selfP.hp) return false; // unlike clearStatus, gives cure message if (selfP.status) { selfB.add('-curestatus', selfP, selfP.status); selfP.setStatus(''); } }; this.setStatus = function(status, source, sourceEffect, ignoreImmunities) { if (!selfP.hp) return false; status = selfB.getEffect(status); if (selfB.event) { if (!source) source = selfB.event.source; if (!sourceEffect) sourceEffect = selfB.effect; } if (!ignoreImmunities && status.id) { // the game currently never ignores immunities if (!selfP.runImmunity(status.id==='tox'?'psn':status.id)) { selfB.debug('immune to status'); return false; } } if (selfP.status === status.id) return false; var prevStatus = selfP.status; var prevStatusData = selfP.statusData; if (status.id && !selfB.runEvent('SetStatus', selfP, source, sourceEffect, status)) { selfB.debug('set status ['+status.id+'] interrupted'); return false; } selfP.status = status.id; selfP.statusData = {id: status.id, target: selfP}; if (source) selfP.statusData.source = source; if (status.duration) { selfP.statusData.duration = status.duration; } if (status.durationCallback) { selfP.statusData.duration = status.durationCallback.call(selfB, selfP, source, sourceEffect); } if (status.id && !selfB.singleEvent('Start', status, selfP.statusData, selfP, source, sourceEffect)) { selfB.debug('status start ['+status.id+'] interrupted'); // cancel the setstatus selfP.status = prevStatus; selfP.statusData = prevStatusData; return false; } selfP.update(); if (status.id && !selfB.runEvent('AfterSetStatus', selfP, source, sourceEffect, status)) { return false; } return true; }; this.clearStatus = function() { // unlike cureStatus, does not give cure message return selfP.setStatus(''); }; this.getStatus = function() { return selfB.getEffect(selfP.status); }; this.eatItem = function(source, sourceEffect) { if (!selfP.hp || !selfP.isActive) return false; if (!selfP.item) return false; if (!sourceEffect && selfB.effect) sourceEffect = selfB.effect; if (!source && selfB.event && selfB.event.target) source = selfB.event.target; var item = selfP.getItem(); if (selfB.runEvent('UseItem', selfP, null, null, item) && selfB.runEvent('EatItem', selfP, null, null, item)) { selfB.add('-enditem', selfP, item, '[eat]'); selfB.singleEvent('Eat', item, selfP.itemData, selfP, source, sourceEffect); selfP.lastItem = selfP.item; selfP.item = ''; selfP.itemData = {id: '', target: selfP}; selfP.usedItemThisTurn = true; return true; } return false; }; this.useItem = function(source, sourceEffect) { if (!selfP.isActive) return false; if (!selfP.item) return false; if (!sourceEffect && selfB.effect) sourceEffect = selfB.effect; if (!source && selfB.event && selfB.event.target) source = selfB.event.target; var item = selfP.getItem(); if (selfB.runEvent('UseItem', selfP, null, null, item)) { switch (item.id) { case 'redcard': selfB.add('-enditem', selfP, item, '[of] '+source); break; default: if (!item.isGem) { selfB.add('-enditem', selfP, item); } break; } selfB.singleEvent('Use', item, selfP.itemData, selfP, source, sourceEffect); selfP.lastItem = selfP.item; selfP.item = ''; selfP.itemData = {id: '', target: selfP}; selfP.usedItemThisTurn = true; return true; } return false; }; this.takeItem = function(source) { if (!selfP.hp || !selfP.isActive) return false; if (!selfP.item) return false; if (!source) source = selfP; var item = selfP.getItem(); if (selfB.runEvent('TakeItem', selfP, source, null, item)) { selfP.lastItem = ''; selfP.item = ''; selfP.itemData = {id: '', target: selfP}; return item; } return false; }; this.setItem = function(item, source, effect) { if (!selfP.hp || !selfP.isActive) return false; item = selfB.getItem(item); selfP.lastItem = selfP.item; selfP.item = item.id; selfP.itemData = {id: item.id, target: selfP}; if (item.id) { selfB.singleEvent('Start', item, selfP.itemData, selfP, source, effect); } if (selfP.lastItem) selfP.usedItemThisTurn = true; return true; }; this.getItem = function() { return selfB.getItem(selfP.item); }; this.clearItem = function() { return selfP.setItem(''); }; this.setAbility = function(ability, source, effect) { if (!selfP.hp) return false; ability = selfB.getAbility(ability); if (selfP.ability === ability.id) { return false; } if (ability.id === 'Multitype' || ability.id === 'Illusion' || selfP.ability === 'Multitype') { return false; } selfP.ability = ability.id; selfP.abilityData = {id: ability.id, target: selfP}; if (ability.id) { selfB.singleEvent('Start', ability, selfP.abilityData, selfP, source, effect); } return true; }; this.getAbility = function() { return selfB.getAbility(selfP.ability); }; this.clearAbility = function() { return selfP.setAbility(''); }; this.getNature = function() { return selfB.getNature(selfP.set.nature); }; this.addVolatile = function(status, source, sourceEffect) { if (!selfP.hp) return false; status = selfB.getEffect(status); if (selfB.event) { if (!source) source = selfB.event.source; if (!sourceEffect) sourceEffect = selfB.effect; } if (selfP.volatiles[status.id]) { selfB.singleEvent('Restart', status, selfP.volatiles[status.id], selfP, source, sourceEffect); return false; } if (!selfP.runImmunity(status.id)) return false; var result = selfB.runEvent('TryAddVolatile', selfP, source, sourceEffect, status); if (!result) { selfB.debug('add volatile ['+status.id+'] interrupted'); return result; } selfP.volatiles[status.id] = {id: status.id}; selfP.volatiles[status.id].target = selfP; if (source) { selfP.volatiles[status.id].source = source; selfP.volatiles[status.id].sourcePosition = source.position; } if (sourceEffect) { selfP.volatiles[status.id].sourceEffect = sourceEffect; } if (status.duration) { selfP.volatiles[status.id].duration = status.duration; } if (status.durationCallback) { selfP.volatiles[status.id].duration = status.durationCallback.call(selfB, selfP, source, sourceEffect); } if (!selfB.singleEvent('Start', status, selfP.volatiles[status.id], selfP, source, sourceEffect)) { // cancel delete selfP.volatiles[status.id]; return false; } selfP.update(); return true; }; this.getVolatile = function(status) { status = selfB.getEffect(status); if (!selfP.volatiles[status.id]) return null; return status; }; this.removeVolatile = function(status) { if (!selfP.hp) return false; status = selfB.getEffect(status); if (!selfP.volatiles[status.id]) return false; selfB.singleEvent('End', status, selfP.volatiles[status.id], selfP); delete selfP.volatiles[status.id]; selfP.update(); return true; }; this.hpPercent = function(d) { //return Math.floor(Math.floor(d*48/selfP.maxhp + 0.5)*100/48); return Math.floor(d*100/selfP.maxhp + 0.5); }; this.getHealth = function(realHp) { if (selfP.fainted) return ' (0 fnt)'; //var hpp = Math.floor(48*selfP.hp/selfP.maxhp) || 1; var hpstring; if (realHp) { hpstring = ''+selfP.hp+'/'+selfP.maxhp; } else { var hpp = Math.floor(selfP.hp*100/selfP.maxhp + 0.5) || 1; if (!selfP.hp) hpp = 0; hpstring = ''+hpp+'/100'; } var status = ''; if (selfP.status) status = ' '+selfP.status; return ' ('+hpstring+status+')'; }; this.hpChange = function(d) { return ''+selfP.hpPercent(d)+selfP.getHealth(); }; this.runImmunity = function(type, message) { if (selfP.fainted) { return false; } if (!type || type === '???') { return true; } if (selfP.negateImmunity[type]) return true; if (!selfP.negateImmunity['Type'] && !selfB.getImmunity(type, selfP)) { selfB.debug('natural immunity'); if (message) { selfB.add('-immune', selfP, '[msg]'); } return false; } var immunity = selfB.runEvent('Immunity', selfP, null, null, type); if (!immunity) { selfB.debug('artificial immunity'); if (message && immunity !== null) { selfB.add('-immune', selfP, '[msg]'); } return false; } return true; }; this.destroy = function() { // deallocate ourself // get rid of some possibly-circular references side = null; selfB = null; selfS = null; selfP.side = null; selfP = null; }; selfP.clearVolatile(true); } function BattleSide(name, battle, n, team) { var selfB = battle; var selfS = this; this.battle = battle; this.n = n; this.name = name; this.isActive = false; this.pokemon = []; this.pokemonLeft = 0; this.active = [null]; this.decision = null; this.ackRequest = -1; this.foe = null; this.sideConditions = {}; this.id = (n?'p2':'p1'); switch (battle.gameType) { case 'doubles': this.active = [null, null]; break; } this.team = selfB.getTeam(this, team); for (var i=0; i>> 0; // truncate the result to the last 32 bits var result = selfB.seed >>> 16; // the first 16 bits of the seed are the random value m = Math.floor(m); n = Math.floor(n); return (m ? (n ? (result%(n-m))+m : result%m) : result/0x10000); }; this.setWeather = function(status, source, sourceEffect) { status = selfB.getEffect(status); if (sourceEffect === undefined && selfB.effect) sourceEffect = selfB.effect; if (source === undefined && selfB.event && selfB.event.target) source = selfB.event.target; if (selfB.weather === status.id) return false; if (selfB.weather && !status.id) { var oldstatus = selfB.getWeather(); selfB.singleEvent('End', oldstatus, selfB.weatherData, selfB); } var prevWeather = selfB.weather; var prevWeatherData = selfB.weatherData; selfB.weather = status.id; selfB.weatherData = {id: status.id}; if (source) { selfB.weatherData.source = source; selfB.weatherData.sourcePosition = source.position; } if (status.duration) { selfB.weatherData.duration = status.duration; } if (status.durationCallback) { selfB.weatherData.duration = status.durationCallback.call(selfB, source, sourceEffect); } if (!selfB.singleEvent('Start', status, selfB.weatherData, selfB, source, sourceEffect)) { selfB.weather = prevWeather; selfB.weatherData = prevWeatherData; return false; } selfB.update(); return true; }; this.clearWeather = function() { return selfB.setWeather(''); }; this.effectiveWeather = function(target) { if (selfB.event) { if (!target) target = selfB.event.target; } if (!selfB.runEvent('TryWeather', target)) return ''; return this.weather; }; this.isWeather = function(weather, target) { var ourWeather = selfB.effectiveWeather(target); if (!Array.isArray(weather)) { return ourWeather === toId(weather); } return (weather.map(toId).indexOf(ourWeather) >= 0); }; this.getWeather = function() { return selfB.getEffect(selfB.weather); }; this.getFormat = function() { return selfB.getEffect(selfB.format); }; this.addPseudoWeather = function(status, source, sourceEffect) { status = selfB.getEffect(status); if (selfB.pseudoWeather[status.id]) { selfB.singleEvent('Restart', status, selfB.pseudoWeather[status.id], selfB, source, sourceEffect); return false; } selfB.pseudoWeather[status.id] = {id: status.id}; if (source) { selfB.pseudoWeather[status.id].source = source; selfB.pseudoWeather[status.id].sourcePosition = source.position; } if (status.duration) { selfB.pseudoWeather[status.id].duration = status.duration; } if (status.durationCallback) { selfB.pseudoWeather[status.id].duration = status.durationCallback.call(selfB, source, sourceEffect); } if (!selfB.singleEvent('Start', status, selfB.pseudoWeather[status.id], selfB, source, sourceEffect)) { delete selfB.pseudoWeather[status.id]; return false; } selfB.update(); return true; }; this.getPseudoWeather = function(status) { status = selfB.getEffect(status); if (!selfB.pseudoWeather[status.id]) return null; return status; }; this.removePseudoWeather = function(status) { status = selfB.getEffect(status); if (!selfB.pseudoWeather[status.id]) return false; selfB.singleEvent('End', status, selfB.pseudoWeather[status.id], selfB); delete selfB.pseudoWeather[status.id]; selfB.update(); return true; }; this.lastMove = ''; this.activeMove = null; this.activePokemon = null; this.activeTarget = null; this.setActiveMove = function(move, pokemon, target) { if (!move) move = null; if (!pokemon) pokemon = null; if (!target) target = pokemon; selfB.activeMove = move; selfB.activePokemon = pokemon; selfB.activeTarget = target; // Mold Breaker and the like selfB.update(); }; this.clearActiveMove = function(failed) { if (selfB.activeMove) { if (!failed) { selfB.lastMove = selfB.activeMove.id; } selfB.activeMove = null; selfB.activePokemon = null; selfB.activeTarget = null; // Mold Breaker and the like, again selfB.update(); } }; this.update = function() { var actives = selfB.p1.active; for (var i=0; i= 5) { // oh fuck this.add('message STACK LIMIT EXCEEDED'); this.add('message PLEASE TELL AESOFT'); this.add('message Event: '+eventid); this.add('message Parent event: '+selfB.event.id); return false; } //this.add('Event: '+eventid+' (depth '+selfB.eventDepth+')'); effect = selfB.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]) { selfB.debug(eventid+' handler suppressed by Klutz or Magic Room'); return true; } if (target.ignore && target.ignore[effect.effectType+'Target']) { selfB.debug(eventid+' handler suppressed by Air Lock'); return true; } if (typeof effect['on'+eventid] === 'undefined') return true; var parentEffect = selfB.effect; var parentEffectData = selfB.effectData; var parentEvent = selfB.event; selfB.effect = effect; selfB.effectData = effectData; selfB.event = {id: eventid, target: target, source: source, effect: sourceEffect}; selfB.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(selfB, args); } else { returnVal = effect['on'+eventid]; } selfB.eventDepth--; selfB.effect = parentEffect; selfB.effectData = parentEffectData; selfB.event = parentEvent; if (typeof returnVal === 'undefined') return true; return returnVal; }; this.runEvent = function(eventid, target, source, effect, relayVar) { if (selfB.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: '+selfB.event.id); return false; } if (!target) target = selfB; var statuses = selfB.getRelevantEffects(target, 'on'+eventid, 'onSource'+eventid, source); var hasRelayVar = true; effect = selfB.getEffect(effect); var args = [target, source, effect]; //console.log('Event: '+eventid+' (depth '+selfB.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; i= target.maxhp) return 0; damage = target.heal(damage, source, effect); switch (effect.id) { case 'leechseed': case 'rest': selfB.add('-heal', target, target.hpChange(damage), '[silent]'); break; case 'drain': selfB.add('-heal', target, target.hpChange(damage), '[from] drain', '[of] '+source); break; case 'wish': break; default: if (effect.effectType === 'Move') { selfB.add('-heal', target, target.hpChange(damage)); } else if (source && source !== target) { selfB.add('-heal', target, target.hpChange(damage), '[from] '+effect.fullname, '[of] '+source); } else { selfB.add('-heal', target, target.hpChange(damage), '[from] '+effect.fullname); } break; } selfB.runEvent('Heal', target, source, effect, damage); return damage; }; this.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); }; this.getDamage = function(pokemon, target, move, suppressMessages) { if (typeof move === 'string') move = selfB.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.damageCallback) { return move.damageCallback.call(selfB, pokemon, target); } if (move.damage === 'level') { return pokemon.level; } if (move.damage) { return move.damage; } if (!move) { move = {}; } if (!move.category) move.category = 'Physical'; if (!move.defensiveCategory) move.defensiveCategory = move.category; if (!move.type) move.type = '???'; var type = move.type; // '???' is typeless damage: used for Struggle and Confusion etc var basePower = move.basePower; if (move.basePowerCallback) { basePower = move.basePowerCallback.call(selfB, pokemon, target, move); } if (!basePower) 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 = (selfB.random(critMult[move.critRatio]) === 0); } } if (move.crit) { move.crit = selfB.runEvent('CriticalHit', target, null, move); } // happens after crit calculation if (basePower) { basePower = selfB.runEvent('BasePower', pokemon, target, move, basePower); if (move.basePowerModifier) { 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(move.category==='Physical'?'atk':'spa'); var defense = defender.getStat(move.defensiveCategory==='Physical'?'def':'spd'); if (move.crit) { move.ignoreNegativeOffensive = true; move.ignorePositiveDefensive = true; } if (move.ignoreNegativeOffensive && attack < attacker.getStat(move.category==='Physical'?'atk':'spa', true)) { move.ignoreOffensive = true; } if (move.ignoreOffensive) { selfB.debug('Negating (sp)atk boost/penalty.'); attack = attacker.getStat(move.category==='Physical'?'atk':'spa', true); } if (move.ignorePositiveDefensive && defense > target.getStat(move.defensiveCategory==='Physical'?'def':'spd', true)) { move.ignoreDefensive = true; } if (move.ignoreDefensive) { selfB.debug('Negating (sp)def boost/penalty.'); defense = target.getStat(move.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; selfB.debug('Spread modifier: ' + spreadModifier); baseDamage = selfB.modify(baseDamage, spreadModifier); } // weather modifier (TODO: relocate here) // crit if (move.crit) { if (!suppressMessages) selfB.add('-crit', target); baseDamage = selfB.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 - selfB.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 = selfB.modify(baseDamage, move.stab || 1.5); } // types var totalTypeMod = selfB.getEffectiveness(type, target); if (totalTypeMod > 0) { if (!suppressMessages) selfB.add('-supereffective', target); baseDamage *= 2; if (totalTypeMod >= 2) { baseDamage *= 2; } } if (totalTypeMod < 0) { if (!suppressMessages) selfB.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. */ this.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; }; this.validTarget = function(target, source, targetType) { var targetLoc; if (target.side == source.side) { targetLoc = -(target.position+1); } else { targetLoc = target.position+1; } return selfB.validTargetLoc(targetLoc, source, targetType); }; this.getTarget = function(decision) { var move = selfB.getMove(decision.move); var target; if ((move.target !== 'randomNormal') && selfB.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 = selfB.resolveTarget(decision.pokemon, decision.move); decision.targetSide = target.side; decision.targetPosition = target.position; } return decision.targetSide.active[decision.targetPosition]; }; this.resolveTarget = function(pokemon, move) { move = selfB.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]; }; this.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 = selfB.p1.active.map(isFainted); var p2fainted = selfB.p2.active.map(isFainted); }; this.queue = []; this.faintQueue = []; this.currentRequest = ''; this.rqid = 0; this.faintMessages = function() { while (selfB.faintQueue.length) { var faintData = selfB.faintQueue.shift(); if (!faintData.target.fainted) { selfB.add('faint', faintData.target); selfB.runEvent('Faint', faintData.target, faintData.source, faintData.effect); faintData.target.fainted = true; faintData.target.isActive = false; faintData.target.isStarted = false; faintData.target.side.pokemonLeft--; } } if (!selfB.p1.pokemonLeft && !selfB.p2.pokemonLeft) { selfB.win(); return true; } if (!selfB.p1.pokemonLeft) { selfB.win(selfB.p2); return true; } if (!selfB.p2.pokemonLeft) { selfB.win(selfB.p1); return true; } return false; }; this.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) { selfB.eachEvent('Priority', null, currentPriority); currentPriority--; } */ selfB.runDecision(decision); if (selfB.currentRequest) { return; } // if (!selfB.queue.length || selfB.queue[0].choice === 'runSwitch') { // if (selfB.faintMessages()) return; // } if (selfB.ended) return; } selfB.nextTurn(); selfB.midTurn = false; selfB.queue = []; }; this.changeDecision = function(pokemon, decision) { selfB.cancelDecision(pokemon); if (!decision.pokemon) decision.pokemon = pokemon; selfB.addQueue(decision); }; /** * Takes a choice string passed from the client. Starts the next * turn if all required choices have been made. */ this.choose = function(sideid, choice, rqid) { var side = null; if (sideid === 'p1' || sideid === 'p2') side = selfB[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) !== selfB.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 = selfB.parseChoice(choice, side); if (selfB.p1.decision && selfB.p2.decision) { if (selfB.p1.decision !== true) { selfB.addQueue(selfB.p1.decision, true, selfB.p1); } if (selfB.p2.decision !== true) { selfB.addQueue(selfB.p2.decision, true, selfB.p2); } selfB.currentRequest = ''; selfB.p1.currentRequest = ''; selfB.p2.currentRequest = ''; selfB.p1.decision = true; selfB.p2.decision = true; selfB.go(); } }; this.undoChoice = function(sideid) { var side = null; if (sideid === 'p1' || sideid === 'p2') side = selfB[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; 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. */ this.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.pokemon[i].trapped && side.currentRequest === 'move') { selfB.debug("Can't switch: The active pokemon is trapped"); return false; } 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]) { selfB.debug("Can't switch: You can't switch to a pokemon that doesn't exist"); return false; } if (data == i) { selfB.debug("Can't switch: You can't switch to yourself"); return false; } if (selfB.battleType !== 'triples' && data < side.active.length) { selfB.debug("Can't switch: You can't switch to an active pokemon except in triples"); return false; } if (side.pokemon[data].fainted) { selfB.debug("Can't switch: You can't switch to a fainted pokemon"); return false; } if (prevSwitches[data]) { selfB.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 = selfB.getMove(move).id; decisions.push({ choice: 'move', pokemon: pokemon, targetLoc: targetLoc, move: move }); break; } } return decisions; }; this.add = function() { selfB.log.push('|'+Array.prototype.slice.call(arguments).join('|')); }; this.lastMoveLine = 0; this.addMove = function() { selfB.lastMoveLine = selfB.log.length; selfB.log.push('|'+Array.prototype.slice.call(arguments).join('|')); }; this.attrLastMove = function() { selfB.log[selfB.lastMoveLine] += '|'+Array.prototype.slice.call(arguments).join('|'); }; this.debug = function(activity) { if (selfB.getFormat().debug) { selfB.add('debug', activity); } }; this.debugError = function(activity) { selfB.add('debug', activity); }; // players this.join = function(slot, name, avatar, team) { if (selfB.p1 && selfB.p1.isActive && selfB.p2 && selfB.p2.isActive) return false; if ((selfB.p1 && selfB.p1.isActive && selfB.p1.name === name) || (selfB.p2 && selfB.p2.isActive && selfB.p2.name === name)) return false; if (selfB.p1 && selfB.p1.isActive || slot === 'p2') { if (selfB.started) { selfB.p2.name = name; } else { //console.log("NEW SIDE: "+name); selfB.p2 = new BattleSide(name, selfB, 1, team); selfB.sides[1] = selfB.p2; } if (avatar) selfB.p2.avatar = avatar; selfB.p2.isActive = true; selfB.add('player', 'p2', selfB.p2.name, avatar); } else { if (selfB.started) { selfB.p1.name = name; } else { //console.log("NEW SIDE: "+name); selfB.p1 = new BattleSide(name, selfB, 0, team); selfB.sides[0] = selfB.p1; } if (avatar) selfB.p1.avatar = avatar; selfB.p1.isActive = true; selfB.add('player', 'p1', selfB.p1.name, avatar); } selfB.start(); return true; }; this.rename = function(slot, name, avatar) { if (slot === 'p1' || slot === 'p2') { var side = selfB[slot]; side.name = name; if (avatar) side.avatar = avatar; selfB.add('player', slot, name, side.avatar); } }; this.leave = function(slot) { if (slot === 'p1' || slot === 'p2') { var side = selfB[slot]; side.emitUpdate({side:'none'}); side.isActive = false; selfB.add('player', slot); selfB.active = false; } return true; }; // IPC this.messageLog = []; // Messages sent by this function are received and handled in // Simulator.prototype.receive in simulator.js (in another process). this.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. this.receive = function(data, more) { this.messageLog.push(data.join(' ')); var logPos = selfB.log.length; var alreadyEnded = selfB.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; } if (selfB.p1 && selfB.p2) { var inactiveSide = -1; if (!selfB.p1.isActive && selfB.p2.isActive) { inactiveSide = 0; } else if (selfB.p1.isActive && !selfB.p2.isActive) { inactiveSide = 1; } else if (!selfB.p1.decision && selfB.p2.decision) { inactiveSide = 0; } else if (selfB.p1.decision && !selfB.p2.decision) { inactiveSide = 1; } if (inactiveSide !== selfB.inactiveSide) { this.send('inactiveside', inactiveSide); selfB.inactiveSide = inactiveSide; } } if (selfB.log.length > logPos) { if (selfB.ended && !alreadyEnded) { if (selfB.rated) { var log = { turns: selfB.turn, p1: selfB.p1.name, p2: selfB.p2.name, p1team: selfB.p1.team, p2team: selfB.p2.team, log: selfB.log } this.send('log', JSON.stringify(log)); } this.send('winupdate', [selfB.winner].concat(selfB.log.slice(logPos))); } else { this.send('update', selfB.log.slice(logPos)); } } }; this.destroy = function() { // deallocate ourself // deallocate children and get rid of references to them for (var i=0; i