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 (typeof text === 'number') text = ''+text;
if (text && text.id) text = text.id;
text = string(text);
if (typeof text !== 'string') return ''; //???
return text.toLowerCase().replace(/[^a-z0-9]+/g, '');
};
toUserid = toId;
/**
* 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 = {};
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);
}
}
});
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.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.hpType = 'Dark';
this.hpPower = 70;
if (this.set.moves) {
for (var i=0; i 520 * 1.06) {
stat *= 520 / selfP.bst * 1.06; // stat scaling in progress
}
if (selfP.set.bstscale && selfP.bst < 520 * 0.94) {
stat *= 520 / selfP.bst * 0.94; // stat scaling in progress
} */
if (i==='hp') {
continue;
} else {
selfP.unboostedStats[i] = Math.floor(Math.floor(2*stat+selfP.set.ivs[i]+Math.floor(selfP.set.evs[i]/4))*selfP.level / 100 + 5);
}
selfP.stats[i] = selfP.unboostedStats[i];
}
for (var i in selfP.baseBoosts) {
selfP.boosts[i] = selfP.baseBoosts[i];
}
selfB.natureModify(selfP.stats, selfP.set.nature);
for (var i in selfP.stats) {
selfP.stats[i] = Math.floor(selfP.stats[i]);
}
if (init) return;
selfB.runEvent('ModifyStats', selfP, null, null, selfP.stats);
for (var i in selfP.stats) {
selfP.stats[i] = Math.floor(selfP.stats[i]);
selfP.unboostedStats[i] = selfP.stats[i];
}
var boostTable = [1,1.5,2,2.5,3,3.5,4];
for (i in selfP.boosts) {
if (selfP.boosts[i] > 6) selfP.boosts[i] = 6;
if (selfP.boosts[i] < -6) selfP.boosts[i] = -6;
if (i === 'accuracy' || i === 'evasion' || i === 'hp') continue; // hp should never happen
if (selfP.boosts[i] >= 0) {
selfP.stats[i] = Math.floor(selfP.unboostedStats[i] * boostTable[selfP.boosts[i]]);
} else {
selfP.stats[i] = Math.floor(selfP.unboostedStats[i] / boostTable[-selfP.boosts[i]]);
}
}
selfB.runEvent('ModifyPokemon', selfP);
};
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) {
if (moveid.id) moveid = moveid.id;
moveid = toId(moveid);
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.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.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)) {
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;
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.foe = null;
this.sideConditions = {};
this.id = (n?'p2':'p1');
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);
}
console.log('['+move.id+'] bp='+basePower);
if (!basePower) return 0;
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 = move.category==='Physical'?attacker.stats.atk:attacker.stats.spa;
var defense = move.defensiveCategory==='Physical'?defender.stats.def:defender.stats.spd;
if (move.crit) {
move.ignoreNegativeOffensive = true;
move.ignorePositiveDefensive = true;
}
if (move.ignoreNegativeOffensive && attack < (move.category==='Physical'?attacker.unboostedStats.atk:attacker.unboostedStats.spa)) {
move.ignoreOffensive = true;
}
if (move.ignoreOffensive) {
selfB.debug('Negating (sp)atk boost/penalty.');
attack = (move.category==='Physical'?attacker.unboostedStats.atk:attacker.unboostedStats.spa);
}
if (move.ignorePositiveDefensive && defense > (move.defensiveCategory==='Physical'?target.unboostedStats.def:target.unboostedStats.spd)) {
move.ignoreDefensive = true;
}
if (move.ignoreDefensive) {
selfB.debug('Negating (sp)def boost/penalty.');
defense = move.defensiveCategory==='Physical'?target.unboostedStats.def:target.unboostedStats.spd;
}
//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)
// 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);
};
this.getTarget = function(decision) {
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 === 'self' || move.target === 'all' || move.target === 'allies' || move.target === 'allySide' || move.target === 'ally') {
return pokemon;
}
return pokemon.side.foe.active[0];
};
this.checkFainted = function() {
if (selfB.p1.active[0].fainted || selfB.p2.active[0].fainted) {
selfB.callback('switch');
return true;
}
return false;
};
this.queue = [];
this.faintQueue = [];
this.decisionWaiting = false;
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) {
if (decision) {
if (!decision.side && decision.pokemon) decision.side = decision.pokemon.side;
if (!decision.choice && decision.move) decision.choice = 'move';
if (!decision.priority) {
var priorities = {
'beforeTurn': 100,
'beforeTurnMove': 99,
'switch': 6,
'runSwitch': 6.1,
'residual': -100,
'team': 102,
'start': 101
};
if (priorities[decision.choice]) {
decision.priority = priorities[decision.choice];
}
}
if (decision.choice === 'move') {
if (selfB.getMove(decision.move).beforeTurnCallback) {
selfB.addQueue({choice: 'beforeTurnMove', pokemon: decision.pokemon, move: decision.move}, true);
}
} else if (decision.choice === 'switch' && !decision.speed) {
if (decision.side.active[0].isActive) decision.speed = decision.side.active[0].stats.spe;
}
if (decision.move) {
var target;
if (!decision.targetPosition) {
target = selfB.resolveTarget(decision.pokemon, decision.move);
decision.targetSide = target.side;
decision.targetPosition = target.position;
}
decision.move = selfB.getMove(decision.move);
if (!decision.priority) {
var priority = decision.move.priority;
priority = selfB.runEvent('ModifyPriority', decision.pokemon, target, decision.move, priority);
decision.priority = priority;
}
}
if (!decision.pokemon && !decision.speed) decision.speed = 1;
if (!decision.speed && decision.newPokemon) decision.speed = decision.newPokemon.stats.spe;
if (!decision.speed) decision.speed = decision.pokemon.stats.spe;
if (decision.choice === 'switch' && !decision.side.pokemon[0].isActive) {
// if there's no actives, switches happen before activations
decision.priority = 6.2;
}
selfB.queue.push(decision);
}
if (!noSort) {
selfB.queue.sort(selfB.comparePriority);
}
};
this.willAct = function() {
for (var i=0; i= 6 || i < 0) return;
if (i == 0) return;
var pokemon = decision.side.pokemon[i];
if (!pokemon) return;
decision.side.pokemon[i] = decision.side.pokemon[0];
decision.side.pokemon[0] = pokemon;
decision.side.pokemon[i].position = i;
decision.side.pokemon[0].position = 0;
return;
// we return here because the update event would crash since there are no active pokemon yet
break;
case 'switch':
if (decision.pokemon) {
decision.pokemon.beingCalledBack = true;
var lastMove = selfB.getMove(decision.pokemon.lastMove);
if (lastMove.selfSwitch !== 'copyvolatile') {
// Don't run any event handlers if Baton Pass was used.
if (!selfB.runEvent('SwitchOut', decision.pokemon)) {
// Warning: DO NOT interrupt a switch-out
// if you just want to trap a pokemon.
// To trap a pokemon and prevent it from switching out,
// (e.g. Mean Look, Magnet Pull) use the 'trapped' flag
// instead.
// Note: Nothing in BW or earlier interrupts
// a switch-out.
break;
}
}
}
if (decision.pokemon && !decision.pokemon.hp && !decision.pokemon.fainted) {
break;
}
selfB.switchIn(decision.newPokemon);
//decision.pokemon.runSwitchIn();
break;
case 'runSwitch':
decision.pokemon.isStarted = true;
selfB.singleEvent('Start', decision.pokemon.getAbility(), decision.pokemon.abilityData, decision.pokemon);
selfB.singleEvent('Start', decision.pokemon.getItem(), decision.pokemon.itemData, decision.pokemon);
break;
case 'beforeTurn':
selfB.eachEvent('BeforeTurn');
break;
case 'residual':
selfB.add('');
selfB.clearActiveMove(true);
selfB.residualEvent('Residual');
break;
}
selfB.clearActiveMove();
if (selfB.faintMessages()) return true;
selfB.eachEvent('Update');
if (selfB.p1.active[0].switchFlag) {
if (selfB.canSwitch(selfB.p1)) {
selfB.callback('switch-ally');
return true;
} else {
selfB.p1.active[0].switchFlag = false;
}
}
if (selfB.p2.active[0].switchFlag) {
if (selfB.canSwitch(selfB.p2)) {
selfB.callback('switch-foe');
return true;
} else {
selfB.p2.active[0].switchFlag = false;
}
}
return false;
};
this.go = function() {
selfB.add('');
if (selfB.curCallback) {
selfB.curCallback = '';
}
if (!selfB.midTurn) {
selfB.queue.push({choice:'residual', priority: -100});
selfB.queue.push({choice:'beforeTurn', priority: 100});
selfB.midTurn = true;
}
selfB.addQueue(null);
var currentPriority = 6;
while (selfB.queue.length) {
var decision = selfB.queue.shift();
/* while (decision.priority < currentPriority && currentPriority > -6) {
selfB.eachEvent('Priority', null, currentPriority);
currentPriority--;
} */
selfB.runDecision(decision);
if (selfB.decisionWaiting) {
return;
}
if (!selfB.queue.length || selfB.queue[0].choice === 'runSwitch') {
if (selfB.faintMessages()) return;
}
if (selfB.ended) return;
}
if (selfB.checkFainted()) 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);
};
this.decide = function(sideid, choice, data, recurse) {
if (!selfB.decisionWaiting) {
return;
}
if (!recurse) recurse = 0;
if (recurse > 2) {
console.log('infinite recursion; breaking');
selfB.add('message Stack overflow detected.');
selfB.add('message BATTLE CRASHED.');
selfB.win();
return false; // infinite recursion
}
var side = null;
if (sideid === 'p1' || sideid === 'p2') side = selfB[sideid];
if (!side) return; // wtf
var decision = {side: side, choice: choice, priority: 0, speed: 0};
if (side.decision === true) {
// Don't change a decision if it's not your turn
return;
}
// I believe this is outdated code that has no effect besides
// exacerbating race conditions
//selfB.cancelDecision(side.active[0]);
if (choice === 'undo') {
side.decision = null;
return;
} else if (choice === 'team') {
if (selfB.curCallback !== 'team-preview') {
return; // hacker!
}
if (!data) {
return; // hacker!
}
decision.team = data;
side.decision = decision;
} else if (choice === 'switch') {
//console.log('switching to '+data);
data = parseInt(data, 10);
if (data < 0) data = 0;
if (data > side.pokemon.length-1) data = side.pokemon.length-1;
var pokemon = side.pokemon[data];
if (pokemon && !pokemon.fainted) {
decision.priority = 6;
decision.newPokemon = pokemon;
// todo: wtf
if (!side.active[0]) {
selfB.debug("The game hasn't started yet");
selfB.decide(sideid, 'move', 'no clue', recurse+1);
return;
}
decision.pokemon = side.active[0];
if (side.active[0].trapped && selfB.curCallback === 'move') {
// hacker!
selfB.debug("Can't switch: The active pokemon is trapped");
selfB.decide(sideid, 'move', 'switch trapped', recurse+1);
return;
}
if (decision.pokemon === pokemon) {
// no
selfB.debug("Can't switch: You can't switch to an active pokemon");
selfB.decide(sideid, 'move', 'null switch', recurse+1);
return;
}
side.decision = decision;
} else {
selfB.debug("Can't switch: This pokemon doesn't exist");
// hacker!
selfB.decide(sideid, 'move', 'pokemon doesnt exist', recurse+1);
return;
}
}
if (choice === 'move') {
if (selfB.curCallback !== 'move') {
return; // hacker!
}
var move = selfB.getMove(data);
if (move) {
if (side.active[0].fainted) {
// hacker!
var pokemon = selfB.getRandomSwitchable(side);
if (!pokemon) {
// double wtf; the game should be ended
// by this point
selfB.add('message A player has no usable pokemon, but the battle didn\'t end.');
selfB.add('message BATTLE CRASHED.');
selfB.win();
return;
}
selfB.debug('Hacking detected, using fainted pokemon\'s move; forcing switch to '+pokemon.id);
selfB.decide(sideid, 'switch', pokemon.id, recurse+1);
}
if (!side.active[0].canUseMove(move)) {
var badmove = move;
// hacker!
move = side.active[0].getValidMoves()[0];
if (move !== 'Struggle' && move !== 'Recharge') {
selfB.debug('Hacking detected, '+badmove.id+' not allowed; forcing '+move);
}
}
decision.pokemon = side.active[0];
decision.move = move;
side.decision = decision;
} else {
// hacker!
selfB.debug('Move not found: '+data);
selfB.decide(sideid, 'move', 'switch trapped', recurse+1);
return;
}
}
if (selfB.p1.decision && selfB.p2.decision) {
selfB.decisionWaiting = false;
if (selfB.p1.decision !== true) {
selfB.addQueue(selfB.p1.decision, true);
}
if (selfB.p2.decision !== true) {
selfB.addQueue(selfB.p2.decision, true);
}
selfB.p1.decision = true;
selfB.p2.decision = true;
selfB.go();
}
};
this.add = function() {
selfB.log.push('|'+Array.prototype.slice.call(arguments).join('|'));
};
this.debug = function(activity) {
if (selfB.getFormat(selfB.format).debug) {
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 || 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.send = function(type, data) {
if (Array.isArray(data)) data = data.join("\n");
process.send(this.id+"\n"+type+"\n"+data);
};
this.receive = function(data, more) {
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 'move':
case 'switch':
case 'undo':
case 'team':
this.decide(data[2], data[1], data[3]);
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.send('update', '|chat|server|<<< '+eval(data[2]));
} catch (e) {
this.send('update', '|chatmsg|<<< error: '+e.message);
}
return;
break;
}
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));
}
}
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;
}
}
};
this.destroy = function() {
// deallocate ourself
// deallocate children and get rid of references to them
for (var i=0; i