/** * 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 */ 'use strict'; const Tools = require('./tools'); const PRNG = require('./prng'); class BattlePokemon { constructor(set, side) { this.side = side; this.battle = side.battle; let pokemonScripts = this.battle.data.Scripts.pokemon; if (pokemonScripts) Object.assign(this, pokemonScripts); if (typeof set === 'string') set = {name: set}; // "pre-bound" functions for nicer syntax (avoids repeated use of `bind`) this.getHealth = (this.getHealth || BattlePokemon.getHealth).bind(this); this.getDetails = (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('Unown'); } this.species = Tools.getSpecies(set.species); if (set.name === set.species || !set.name) { set.name = this.baseTemplate.baseSpecies; } this.name = set.name.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.maybeTrapped = false; this.maybeDisabled = false; this.illusion = null; this.fainted = false; this.faintQueued = false; this.lastItem = ''; this.ateBerry = false; this.status = ''; this.position = 0; this.lastMove = ''; this.moveThisTurn = ''; this.lastDamage = 0; this.lastAttackedBy = null; 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.duringMove = false; this.speed = 0; this.abilityOrder = 0; set.level = this.battle.clampIntRange(set.forcedLevel || set.level || 100, 1, 9999); this.level = set.level; let genders = {M:'M', F:'F', N:'N'}; this.gender = genders[set.gender] || this.template.gender || (Math.random() * 2 < 1 ? 'M' : 'F'); if (this.gender === 'N') this.gender = ''; this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255; this.pokeball = this.set.pokeball || 'pokeball'; 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.height = this.template.height; this.heightm = this.template.heightm; this.weight = this.template.weight; this.weightkg = this.template.weightkg; 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; this.addedType = ''; this.knownType = true; if (this.set.moves) { for (let i = 0; i < this.set.moves.length; i++) { let move = this.battle.getMove(this.set.moves[i]); if (!move.id) continue; if (move.id === 'hiddenpower' && move.type !== 'Normal') { if (!set.hpType) set.hpType = move.type; move = this.battle.getMove('hiddenpower'); } this.baseMoveset.push({ move: move.name, id: move.id, pp: ((move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5), maxpp: ((move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5), target: move.target, disabled: false, disabledSource: '', used: false, }); this.moves.push(move.id); } } this.canMegaEvo = this.battle.canMegaEvo(this); if (!this.set.evs) { this.set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; } if (!this.set.ivs) { this.set.ivs = {hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31}; } let stats = {hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31}; for (let i in stats) { if (!this.set.evs[i]) this.set.evs[i] = 0; if (!this.set.ivs[i] && this.set.ivs[i] !== 0) this.set.ivs[i] = 31; } for (let i in this.set.evs) { this.set.evs[i] = this.battle.clampIntRange(this.set.evs[i], 0, 255); } for (let i in this.set.ivs) { this.set.ivs[i] = this.battle.clampIntRange(this.set.ivs[i], 0, 31); } if (this.battle.gen && this.battle.gen <= 2) { // We represent DVs using even IVs. Ensure they are in fact even. for (let i in this.set.ivs) { this.set.ivs[i] &= 30; } } let hpData = this.battle.getHiddenPower(this.set.ivs); this.hpType = set.hpType || hpData.type; this.hpPower = hpData.power; this.boosts = {atk: 0, def: 0, spa: 0, spd: 0, spe: 0, accuracy: 0, evasion: 0}; this.stats = {atk:0, def:0, spa:0, spd:0, spe:0}; this.baseStats = this.battle.spreadModify(this.template.baseStats, this.set); // This is used in gen 1 only, here to avoid code repetition. // Only declared if gen 1 to avoid declaring an object we aren't going to need. if (this.battle.gen === 1) this.modifiedStats = {atk:0, def:0, spa:0, spd:0, spe:0}; this.maxhp = this.template.maxHP || this.baseStats.hp; this.hp = this.hp || this.maxhp; this.isStale = 0; this.isStaleCon = 0; this.isStaleHP = this.maxhp; this.isStalePPTurns = 0; // Transform copies IVs in gen 4 and earlier, so we track the base IVs/HP-type/power this.baseIvs = this.set.ivs; this.baseHpType = this.hpType; this.baseHpPower = this.hpPower; this.clearVolatile(true); } toString() { let fullname = this.fullname; if (this.illusion) fullname = this.illusion.fullname; let positionList = 'abcdef'; if (this.isActive) return fullname.substr(0, 2) + positionList[this.position] + fullname.substr(2); return fullname; } static getDetails(side) { if (this.illusion) return this.illusion.details + '|' + this.getHealth(side); return this.details + '|' + this.getHealth(side); } updateSpeed() { this.speed = this.getDecisionSpeed(); } calculateStat(statName, boost, modifier) { statName = toId(statName); if (statName === 'hp') return this.maxhp; // please just read .maxhp directly // base stat let stat = this.stats[statName]; // Wonder Room swaps defenses before calculating anything else if ('wonderroom' in this.battle.pseudoWeather) { if (statName === 'def') { stat = this.stats['spd']; } else if (statName === 'spd') { stat = this.stats['def']; } } // stat boosts // boost = this.boosts[statName]; let boosts = {}; boosts[statName] = boost; boosts = this.battle.runEvent('ModifyBoost', this, null, null, boosts); boost = boosts[statName]; let boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4]; if (boost > 6) boost = 6; if (boost < -6) boost = -6; if (boost >= 0) { stat = Math.floor(stat * boostTable[boost]); } else { stat = Math.floor(stat / boostTable[-boost]); } // stat modifier stat = this.battle.modify(stat, (modifier || 1)); if (this.battle.getStatCallback) { stat = this.battle.getStatCallback(stat, statName, this); } return stat; } getStat(statName, unboosted, unmodified) { statName = toId(statName); if (statName === 'hp') return this.maxhp; // please just read .maxhp directly // base stat let stat = this.stats[statName]; // Download ignores Wonder Room's effect, but this results in // stat stages being calculated on the opposite defensive stat if (unmodified && 'wonderroom' in this.battle.pseudoWeather) { if (statName === 'def') { statName = 'spd'; } else if (statName === 'spd') { statName = 'def'; } } // stat boosts if (!unboosted) { let boosts = this.battle.runEvent('ModifyBoost', this, null, null, Object.assign({}, this.boosts)); let boost = boosts[statName]; let boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4]; if (boost > 6) boost = 6; if (boost < -6) boost = -6; if (boost >= 0) { stat = Math.floor(stat * boostTable[boost]); } else { stat = Math.floor(stat / boostTable[-boost]); } } // stat modifier effects if (!unmodified) { let statTable = {atk:'Atk', def:'Def', spa:'SpA', spd:'SpD', spe:'Spe'}; stat = this.battle.runEvent('Modify' + statTable[statName], this, null, null, stat); } if (this.battle.getStatCallback) { stat = this.battle.getStatCallback(stat, statName, this, unboosted); } return stat; } getDecisionSpeed() { let speed = this.getStat('spe', false, false); if (speed > 10000) speed = 10000; if (this.battle.getPseudoWeather('trickroom')) { speed = 0x2710 - speed; } return speed & 0x1FFF; } getWeight() { let weight = this.template.weightkg; weight = this.battle.runEvent('ModifyWeight', this, null, null, weight); if (weight < 0.1) weight = 0.1; return weight; } getMoveData(move) { move = this.battle.getMove(move); for (let i = 0; i < this.moveset.length; i++) { let moveData = this.moveset[i]; if (moveData.id === move.id) { return moveData; } } return null; } getMoveTargets(move, target) { let targets = []; switch (move.target) { case 'all': case 'foeSide': case 'allySide': case 'allyTeam': if (!move.target.startsWith('foe')) { for (let i = 0; i < this.side.active.length; i++) { if (!this.side.active[i].fainted) { targets.push(this.side.active[i]); } } } if (!move.target.startsWith('ally')) { for (let i = 0; i < this.side.foe.active.length; i++) { if (!this.side.foe.active[i].fainted) { targets.push(this.side.foe.active[i]); } } } break; case 'allAdjacent': case 'allAdjacentFoes': if (move.target === 'allAdjacent') { for (let i = 0; i < this.side.active.length; i++) { if (this.battle.isAdjacent(this, this.side.active[i])) { targets.push(this.side.active[i]); } } } for (let i = 0; i < this.side.foe.active.length; i++) { if (this.battle.isAdjacent(this, this.side.foe.active[i])) { targets.push(this.side.foe.active[i]); } } break; default: let selectedTarget = target; if (!target || (target.fainted && target.side !== this.side)) { // If a targeted foe faints, the move is retargeted target = this.battle.resolveTarget(this, move); } if (target.side.active.length > 1) { if (!move.flags['charge'] || this.volatiles['twoturnmove'] || (move.id === 'solarbeam' && this.battle.isWeather(['sunnyday', 'desolateland'])) || (this.hasItem('powerherb') && move.id !== 'skydrop')) { target = this.battle.priorityEvent('RedirectTarget', this, this, move, target); } } if (selectedTarget !== target) { this.battle.retargetLastMove(target); } targets = [target]; // Resolve apparent targets for Pressure. if (move.pressureTarget) { // At the moment, this is the only supported target. if (move.pressureTarget === 'foeSide') { for (let i = 0; i < this.side.foe.active.length; i++) { if (this.side.foe.active[i] && !this.side.foe.active[i].fainted) { targets.push(this.side.foe.active[i]); } } } } } return targets; } ignoringAbility() { return !!((this.battle.gen >= 5 && !this.isActive) || (this.volatiles['gastroacid'] && !(this.ability in {comatose:1, multitype:1, schooling:1, stancechange:1}))); } ignoringItem() { return !!((this.battle.gen >= 5 && !this.isActive) || (this.hasAbility('klutz') && !this.getItem().ignoreKlutz) || this.volatiles['embargo'] || this.battle.pseudoWeather['magicroom']); } deductPP(move, amount, source) { move = this.battle.getMove(move); let ppData = this.getMoveData(move); if (!ppData) return false; ppData.used = true; if (!ppData.pp) return false; ppData.pp -= amount || 1; if (ppData.pp <= 0) { ppData.pp = 0; } if (ppData.virtual) { let foeActive = this.side.foe.active; for (let i = 0; i < foeActive.length; i++) { if (foeActive[i].isStale >= 2) { if (move.selfSwitch) this.isStalePPTurns++; return true; } } } this.isStalePPTurns = 0; return true; } moveUsed(move, targetLoc) { this.lastMove = this.battle.getMove(move).id; this.lastMoveTargetLoc = targetLoc; this.moveThisTurn = this.lastMove; } gotAttacked(move, damage, source) { if (!damage) damage = 0; move = this.battle.getMove(move); this.lastAttackedBy = { pokemon: source, damage: damage, move: move.id, thisTurn: true, }; } getLockedMove() { let lockedMove = this.battle.runEvent('LockMove', this); if (lockedMove === true) lockedMove = false; return lockedMove; } getMoves(lockedMove, restrictData) { if (lockedMove) { lockedMove = toId(lockedMove); this.trapped = true; if (lockedMove === 'recharge') { return [{ move: 'Recharge', id: 'recharge', }]; } for (let i = 0; i < this.moveset.length; i++) { let moveEntry = this.moveset[i]; if (moveEntry.id !== lockedMove) continue; return [{ move: moveEntry.move, id: moveEntry.id, }]; } // does this happen? return [{ move: this.battle.getMove(lockedMove).name, id: lockedMove, }]; } let moves = []; let hasValidMove = false; for (let i = 0; i < this.moveset.length; i++) { let moveEntry = this.moveset[i]; let moveName = moveEntry.move; if (moveEntry.id === 'hiddenpower') { moveName = 'Hidden Power ' + this.hpType; if (this.battle.gen < 6) moveName += ' ' + this.hpPower; } else if (moveEntry.id === 'return') { moveName = 'Return ' + this.battle.getMove('return').basePowerCallback(this); } else if (moveEntry.id === 'frustration') { moveName = 'Frustration ' + this.battle.getMove('frustration').basePowerCallback(this); } let target = moveEntry.target; if (moveEntry.id === 'curse') { if (!this.hasType('Ghost')) { target = this.battle.getMove('curse').nonGhostTarget || moveEntry.target; } } let disabled = moveEntry.disabled; if (moveEntry.pp <= 0 || disabled && this.side.active.length >= 2 && this.battle.targetTypeChoices(target)) { disabled = true; } else if (disabled === 'hidden' && restrictData) { disabled = false; } if (!disabled) { hasValidMove = true; } moves.push({ move: moveName, id: moveEntry.id, pp: moveEntry.pp, maxpp: moveEntry.maxpp, target: target, disabled: disabled, }); } if (hasValidMove) return moves; return []; } getRequestData() { let lockedMove = this.getLockedMove(); // Information should be restricted for the last active Pokémon let isLastActive = this.isLastActive(); let moves = this.getMoves(lockedMove, isLastActive); let data = {moves: moves.length ? moves : [{move: 'Struggle', id: 'struggle'}]}; if (isLastActive) { if (this.maybeDisabled) { data.maybeDisabled = true; } if (this.trapped === true) { data.trapped = true; } else if (this.maybeTrapped) { data.maybeTrapped = true; } } else { if (this.trapped) data.trapped = true; } if (!lockedMove) { if (this.canMegaEvo) data.canMegaEvo = true; let canZMove = this.battle.canZMove(this); if (canZMove) data.canZMove = canZMove; } return data; } isLastActive() { if (!this.isActive) return false; let allyActive = this.side.active; for (let i = this.position + 1; i < allyActive.length; i++) { if (allyActive[i] && !allyActive.fainted) return false; } return true; } positiveBoosts() { let boosts = 0; for (let i in this.boosts) { if (this.boosts[i] > 0) boosts += this.boosts[i]; } return boosts; } boostBy(boost) { let delta = 0; for (let i in boost) { 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; } } return delta; } clearBoosts() { for (let i in this.boosts) { this.boosts[i] = 0; } } setBoost(boost) { for (let i in boost) { this.boosts[i] = boost[i]; } } copyVolatileFrom(pokemon) { this.clearVolatile(); this.boosts = pokemon.boosts; for (let i in pokemon.volatiles) { if (this.battle.getEffect(i).noCopy) continue; // shallow clones this.volatiles[i] = Object.assign({}, pokemon.volatiles[i]); if (this.volatiles[i].linkedPokemon) { delete pokemon.volatiles[i].linkedPokemon; delete pokemon.volatiles[i].linkedStatus; this.volatiles[i].linkedPokemon.volatiles[this.volatiles[i].linkedStatus].linkedPokemon = this; } } pokemon.clearVolatile(); for (let i in this.volatiles) { this.battle.singleEvent('Copy', this.getVolatile(i), this.volatiles[i], this); } } transformInto(pokemon, user, effect) { let template = pokemon.template; if (pokemon.fainted || pokemon.illusion || (pokemon.volatiles['substitute'] && this.battle.gen >= 5)) { 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; this.addedType = pokemon.addedType; this.knownType = this.side === pokemon.side && pokemon.knownType; for (let statName in this.stats) { this.stats[statName] = pokemon.stats[statName]; } this.moveset = []; this.moves = []; this.set.ivs = (this.battle.gen >= 5 ? this.set.ivs : pokemon.set.ivs); this.hpType = (this.battle.gen >= 5 ? this.hpType : pokemon.hpType); this.hpPower = (this.battle.gen >= 5 ? this.hpPower : pokemon.hpPower); for (let i = 0; i < pokemon.moveset.length; i++) { let moveData = pokemon.moveset[i]; let moveName = moveData.move; if (moveData.id === 'hiddenpower') { moveName = 'Hidden Power ' + this.hpType; } this.moveset.push({ move: moveName, id: moveData.id, pp: moveData.maxpp === 1 ? 1 : 5, maxpp: this.battle.gen >= 5 ? (moveData.maxpp === 1 ? 1 : 5) : moveData.maxpp, target: moveData.target, disabled: false, used: false, virtual: true, }); this.moves.push(toId(moveName)); } for (let j in pokemon.boosts) { this.boosts[j] = pokemon.boosts[j]; } if (effect) { this.battle.add('-transform', this, pokemon, '[from] ' + effect.fullname); } else { this.battle.add('-transform', this, pokemon); } this.setAbility(pokemon.ability, this, {id: 'transform'}); // Change formes based on held items (for Transform) // Only ever relevant in Generation 4 since Generation 3 didn't have item-based forme changes if (this.battle.gen === 4) { if (this.template.num === 487) { // Giratina formes if (this.template.species === 'Giratina' && this.item === 'griseousorb') { this.formeChange('Giratina-Origin'); this.battle.add('-formechange', this, 'Giratina-Origin'); } else if (this.template.species === 'Giratina-Origin' && this.item !== 'griseousorb') { this.formeChange('Giratina'); this.battle.add('-formechange', this, 'Giratina'); } } if (this.template.num === 493) { // Arceus formes let item = Tools.getItem(this.item); let targetForme = (item && item.onPlate ? 'Arceus-' + item.onPlate : 'Arceus'); if (this.template.species !== targetForme) { this.formeChange(targetForme); this.battle.add('-formechange', this, targetForme); } } } return true; } formeChange(template, dontRecalculateStats) { template = this.battle.getTemplate(template); if (!template.abilities) return false; this.template = template; this.types = template.types; this.addedType = ''; this.knownType = true; if (!dontRecalculateStats) { let stats = this.battle.spreadModify(this.template.baseStats, this.set); for (let statName in this.stats) { this.stats[statName] = stats[statName]; this.baseStats[statName] = stats[statName]; if (this.modifiedStats) this.modifiedStats[statName] = stats[statName]; // Gen 1: Reset modified stats. } if (this.battle.gen <= 1) { // Gen 1: Re-Apply burn and para drops. // FIXME: modifyStat() is only defined for the Gen 1 mod... if (this.status === 'par') this.modifyStat('spe', 0.25); if (this.status === 'brn') this.modifyStat('atk', 0.5); } this.speed = this.stats.spe; } return true; } clearVolatile(init) { this.boosts = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0, accuracy: 0, evasion: 0, }; if (this.battle.gen === 1 && this.baseMoves.includes('mimic') && !this.transformed) { let moveslot = this.baseMoves.indexOf('mimic'); let mimicPP = this.moveset[moveslot] ? this.moveset[moveslot].pp : 16; this.moveset = this.baseMoveset.slice(); this.moveset[moveslot].pp = mimicPP; } else { this.moveset = this.baseMoveset.slice(); } this.moves = this.moveset.map(move => toId(move.move)); this.transformed = false; this.ability = this.baseAbility; this.set.ivs = this.baseIvs; this.hpType = this.baseHpType; this.hpPower = this.baseHpPower; for (let i in this.volatiles) { if (this.volatiles[i].linkedStatus) { this.volatiles[i].linkedPokemon.removeVolatile(this.volatiles[i].linkedStatus); } } this.volatiles = {}; this.switchFlag = false; this.lastMove = ''; this.moveThisTurn = ''; this.lastDamage = 0; this.lastAttackedBy = null; this.newlySwitched = true; this.beingCalledBack = false; this.formeChange(this.baseTemplate); } hasType(type) { if (!type) return false; if (Array.isArray(type)) { for (let i = 0; i < type.length; i++) { if (this.hasType(type[i])) return true; } } else { if (this.getTypes().includes(type)) return true; } return false; } // returns the amount of damage actually dealt faint(source, effect) { // This function only puts the pokemon in the faint queue; // actually setting of this.fainted comes later when the // faint queue is resolved. if (this.fainted || this.faintQueued) return 0; let d = this.hp; this.hp = 0; this.switchFlag = false; this.faintQueued = true; this.battle.faintQueue.push({ target: this, source: source, effect: effect, }); return d; } damage(d, source, effect) { if (!this.hp) return 0; if (d < 1 && d > 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; } tryTrap(isHidden) { if (this.runStatusImmunity('trapped')) { if (this.trapped && isHidden) return true; this.trapped = isHidden ? 'hidden' : true; return true; } return false; } hasMove(moveid) { moveid = toId(moveid); if (moveid.substr(0, 11) === 'hiddenpower') moveid = 'hiddenpower'; for (let i = 0; i < this.moveset.length; i++) { if (moveid === this.battle.getMove(this.moveset[i].move).id) { return moveid; } } return false; } disableMove(moveid, isHidden, sourceEffect) { if (!sourceEffect && this.battle.event) { sourceEffect = this.battle.effect; } moveid = toId(moveid); for (let move of this.moveset) { if (move.id === moveid && move.disabled !== true) { move.disabled = (isHidden || true); move.disabledSource = (sourceEffect ? sourceEffect.fullname : ''); break; } } } // returns the amount of damage actually healed heal(d) { if (!this.hp) return false; d = Math.floor(d); if (isNaN(d)) return false; if (d <= 0) return false; if (this.hp >= this.maxhp) return false; this.hp += d; if (this.hp > this.maxhp) { d -= this.hp - this.maxhp; this.hp = this.maxhp; } return d; } // sets HP, returns delta sethp(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; } trySetStatus(status, source, sourceEffect) { return this.setStatus(this.status || status, source, sourceEffect); } cureStatus(silent) { if (!this.hp) return false; // unlike clearStatus, gives cure message if (this.status) { this.battle.add('-curestatus', this, this.status, silent ? '[silent]' : '[msg]'); this.setStatus(''); } } setStatus(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 (this.status === status.id) { if (sourceEffect && sourceEffect.status === this.status) { this.battle.add('-fail', this, this.status); } else if (sourceEffect && sourceEffect.status) { this.battle.add('-fail', this); } return false; } if (!ignoreImmunities && status.id && !(source && source.hasAbility('corrosion') && status.id in {'tox': 1, 'psn': 1})) { // the game currently never ignores immunities if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) { this.battle.debug('immune to status'); if (sourceEffect && sourceEffect.status) this.battle.add('-immune', this, '[msg]'); return false; } } let prevStatus = this.status; let prevStatusData = this.statusData; if (status.id) { let result = this.battle.runEvent('SetStatus', this, source, sourceEffect, status); if (!result) { this.battle.debug('set status [' + status.id + '] interrupted'); return result; } } 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; } if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) { return false; } return true; } clearStatus() { // unlike cureStatus, does not give cure message return this.setStatus(''); } getStatus() { return this.battle.getEffect(this.status); } eatItem(item, source, sourceEffect) { if (!this.hp || !this.isActive) return false; if (!this.item) return false; let 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('TryEatItem', this, null, null, item)) { this.battle.add('-enditem', this, item, '[eat]'); this.battle.singleEvent('Eat', item, this.itemData, this, source, sourceEffect); this.battle.runEvent('EatItem', this, null, null, item); this.lastItem = this.item; this.item = ''; this.itemData = {id: '', target: this}; this.usedItemThisTurn = true; this.ateBerry = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; } return false; } useItem(item, source, sourceEffect) { if ((!this.hp && !this.getItem().isGem) || !this.isActive) return false; if (!this.item) return false; let 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; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; } return false; } takeItem(source) { if (!this.isActive) return false; if (!this.item) return false; if (!source) source = this; if (this.battle.gen === 4) { if (toId(this.ability) === 'multitype') return false; if (source && toId(source.ability) === 'multitype') return false; } let item = this.getItem(); if (this.battle.runEvent('TakeItem', this, source, null, item)) { this.item = ''; this.itemData = {id: '', target: this}; return item; } return false; } setItem(item, source, effect) { if (!this.hp || !this.isActive) return false; item = this.battle.getItem(item); let effectid; if (this.battle.effect) effectid = this.battle.effect.id; if (item.id === 'leppaberry' && effectid !== 'trick' && effectid !== 'switcheroo') { this.isStale = 2; this.isStaleSource = 'getleppa'; } 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; } getItem() { return this.battle.getItem(this.item); } hasItem(item) { if (this.ignoringItem()) return false; let ownItem = this.item; if (!Array.isArray(item)) { return ownItem === toId(item); } return item.map(toId).includes(ownItem); } clearItem() { return this.setItem(''); } setAbility(ability, source, effect, noForce) { if (!this.hp) return false; ability = this.battle.getAbility(ability); let oldAbility = this.ability; if (noForce && oldAbility === ability.id) { return false; } if (!effect || effect.id !== 'transform') { if (ability.id in {illusion:1, multitype:1, stancechange:1}) return false; if (oldAbility in {multitype:1, stancechange:1}) return false; } this.battle.singleEvent('End', this.battle.getAbility(oldAbility), this.abilityData, this, source, effect); if (!effect && this.battle.effect && this.battle.effect.effectType === 'Move') { this.battle.add('-endability', this, this.battle.getAbility(oldAbility), '[from] move: ' + this.battle.getMove(this.battle.effect.id)); } this.ability = ability.id; this.abilityData = {id: ability.id, target: this}; if (ability.id && this.battle.gen > 3) { this.battle.singleEvent('Start', ability, this.abilityData, this, source, effect); } this.abilityOrder = this.battle.abilityOrder++; return oldAbility; } getAbility() { return this.battle.getAbility(this.ability); } getMegaAbility() { if (!this.canMegaEvo) return null; const megaTemplate = this.battle.getTemplate(this.canMegaEvo); return this.battle.getAbility(megaTemplate.abilities['0']); } hasAbility(ability) { if (this.ignoringAbility()) return false; let ownAbility = this.ability; if (!Array.isArray(ability)) { return ownAbility === toId(ability); } return ability.map(toId).includes(ownAbility); } clearAbility() { return this.setAbility(''); } getNature() { return this.battle.getNature(this.set.nature); } addVolatile(status, source, sourceEffect, linkedStatus) { let result; status = this.battle.getEffect(status); if (!this.hp && !status.affectsFainted) return false; if (linkedStatus && !source.hp) return false; if (this.battle.event) { if (!source) source = this.battle.event.source; if (!sourceEffect) sourceEffect = this.battle.effect; } if (this.volatiles[status.id]) { if (!status.onRestart) return false; return this.battle.singleEvent('Restart', status, this.volatiles[status.id], this, source, sourceEffect); } if (!this.runStatusImmunity(status.id)) return false; 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); } result = this.battle.singleEvent('Start', status, this.volatiles[status.id], this, source, sourceEffect); if (!result) { // cancel delete this.volatiles[status.id]; return result; } if (linkedStatus && source && !source.volatiles[linkedStatus]) { source.addVolatile(linkedStatus, this, sourceEffect, status); source.volatiles[linkedStatus].linkedPokemon = this; source.volatiles[linkedStatus].linkedStatus = status; this.volatiles[status].linkedPokemon = source; this.volatiles[status].linkedStatus = linkedStatus; } return true; } getVolatile(status) { status = this.battle.getEffect(status); if (!this.volatiles[status.id]) return null; return status; } removeVolatile(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); let linkedPokemon = this.volatiles[status.id].linkedPokemon; let linkedStatus = this.volatiles[status.id].linkedStatus; delete this.volatiles[status.id]; if (linkedPokemon && linkedPokemon.volatiles[linkedStatus]) { linkedPokemon.removeVolatile(linkedStatus); } return true; } static getHealth(side) { if (!this.hp) return '0 fnt'; let hpstring; // side === true in replays if (side === this.side || this.battle.reportExactHP || (side === true && this.battle.replayExactHP)) { hpstring = '' + this.hp + '/' + this.maxhp; } else { let ratio = this.hp / this.maxhp; if (this.battle.reportPercentages) { // HP Percentage Mod mechanics let percentage = Math.ceil(ratio * 100); if ((percentage === 100) && (ratio < 1.0)) { percentage = 99; } hpstring = '' + percentage + '/100'; } else { // In-game accurate pixel health mechanics let 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; } /** * Sets a type (except on Arceus, who resists type changes) * newType can be an array, but this is for OMs only. The game in * reality doesn't support setting a type to more than one type. */ setType(newType, enforce) { // First type of Arceus, Silvally cannot be normally changed if (!enforce && (this.template.num === 493 || this.template.num === 773)) return false; if (!newType) throw new Error("Must pass type to setType"); this.types = (typeof newType === 'string' ? [newType] : newType); this.addedType = ''; this.knownType = true; return true; } addType(newType) { // removes any types added previously and adds another one this.addedType = newType; return true; } getTypes(excludeAdded) { let types = this.types; if (!excludeAdded && this.addedType) { types = types.concat(this.addedType); } // If a Fire/Flying type uses Burn Up and Roost, it becomes ???/Flying-type, but it's still grounded. if ('roost' in this.volatiles && !types.includes('???')) { types = types.filter(type => type !== 'Flying'); } if (types.length) return types; return [this.battle.gen >= 5 ? 'Normal' : '???']; } isGrounded(negateImmunity) { if ('gravity' in this.battle.pseudoWeather) return true; if ('ingrain' in this.volatiles) return true; if ('smackdown' in this.volatiles) return true; let item = (this.ignoringItem() ? '' : this.item); if (item === 'ironball') return true; // If a Fire/Flying type uses Burn Up and Roost, it becomes ???/Flying-type, but it's still grounded. if (!negateImmunity && this.hasType('Flying') && !('roost' in this.volatiles)) return false; if (this.hasAbility('levitate') && !this.battle.suppressingAttackEvents()) return null; if ('magnetrise' in this.volatiles) return false; if ('telekinesis' in this.volatiles) return false; return item !== 'airballoon'; } isSemiInvulnerable() { if (this.volatiles['fly'] || this.volatiles['bounce'] || this.volatiles['skydrop'] || this.volatiles['dive'] || this.volatiles['dig'] || this.volatiles['phantomforce'] || this.volatiles['shadowforce']) { return true; } for (let i = 0; i < this.side.foe.active.length; i++) { if (this.side.foe.active[i].volatiles['skydrop'] && this.side.foe.active[i].volatiles['skydrop'].source === this) { return true; } } return false; } runEffectiveness(move) { let totalTypeMod = 0; let types = this.getTypes(); for (let i = 0; i < types.length; i++) { let typeMod = this.battle.getEffectiveness(move, types[i]); typeMod = this.battle.singleEvent('Effectiveness', move, null, types[i], move, null, typeMod); totalTypeMod += this.battle.runEvent('Effectiveness', this, types[i], move, typeMod); } return totalTypeMod; } runImmunity(type, message) { if (!type || type === '???') { return true; } if (!(type in this.battle.data.TypeChart)) { if (type === 'Fairy' || type === 'Dark' || type === 'Steel') return true; throw new Error("Use runStatusImmunity for " + type); } if (this.fainted) { return false; } let isGrounded; let negateResult = this.battle.runEvent('NegateImmunity', this, type); if (type === 'Ground') { isGrounded = this.isGrounded(!negateResult); if (isGrounded === null) { if (message) { this.battle.add('-immune', this, '[msg]', '[from] ability: Levitate'); } return false; } } if (!negateResult) return true; if ((isGrounded === undefined && !this.battle.getImmunity(type, this)) || isGrounded === false) { if (message) { this.battle.add('-immune', this, '[msg]'); } return false; } return true; } runStatusImmunity(type, message) { if (this.fainted) { return false; } if (!type) { return true; } if (!this.battle.getImmunity(type, this)) { this.battle.debug('natural status immunity'); if (message) { this.battle.add('-immune', this, '[msg]'); } return false; } let immunity = this.battle.runEvent('Immunity', this, null, null, type); if (!immunity) { this.battle.debug('artificial status immunity'); if (message && immunity !== null) { this.battle.add('-immune', this, '[msg]'); } return false; } return true; } destroy() { // deallocate ourself // get rid of some possibly-circular references this.battle = null; this.side = null; } } class BattleSide { constructor(name, battle, n, team) { let sideScripts = battle.data.Scripts.side; if (sideScripts) Object.assign(this, sideScripts); this.getChoice = (this.getChoice || BattleSide.getChoice).bind(this); this.battle = battle; this.n = n; this.name = name; this.pokemon = []; this.active = [null]; this.sideConditions = {}; this.isActive = false; this.pokemonLeft = 0; this.faintedLastTurn = false; this.faintedThisTurn = false; this.choiceData = null; this.foe = null; this.id = n ? 'p2' : 'p1'; switch (this.battle.gameType) { case 'doubles': this.active = [null, null]; break; case 'triples': case 'rotation': this.active = [null, null, null]; break; } this.team = this.battle.getTeam(this, team); for (let i = 0; i < this.team.length && i < 6; i++) { //console.log("NEW POKEMON: " + (this.team[i] ? this.team[i].name : '[unidentified]')); this.pokemon.push(new BattlePokemon(this.team[i], this)); } this.pokemonLeft = this.pokemon.length; for (let i = 0; i < this.pokemon.length; i++) { this.pokemon[i].position = i; } } static getChoice(side) { if (side !== this && side !== true) return ''; return this.choiceData.choices.join(', '); } toString() { return this.id + ': ' + this.name; } getData() { let data = { name: this.name, id: this.id, pokemon: [], }; for (let i = 0; i < this.pokemon.length; i++) { let pokemon = this.pokemon[i]; data.pokemon.push({ ident: pokemon.fullname, details: pokemon.details, condition: pokemon.getHealth(pokemon.side), active: (pokemon.position < pokemon.side.active.length), stats: { atk: pokemon.baseStats['atk'], def: pokemon.baseStats['def'], spa: pokemon.baseStats['spa'], spd: pokemon.baseStats['spd'], spe: pokemon.baseStats['spe'], }, moves: pokemon.moves.map(move => { if (move === 'hiddenpower') { return move + toId(pokemon.hpType) + (pokemon.hpPower === 70 ? '' : pokemon.hpPower); } return move; }), baseAbility: pokemon.baseAbility, item: pokemon.item, pokeball: pokemon.pokeball, }); } return data; } randomActive() { let actives = this.active.filter(active => active && !active.fainted); if (!actives.length) return null; let i = Math.floor(Math.random() * actives.length); return actives[i]; } addSideCondition(status, source, sourceEffect) { status = this.battle.getEffect(status); if (this.sideConditions[status.id]) { if (!status.onRestart) return false; return this.battle.singleEvent('Restart', status, this.sideConditions[status.id], this, source, sourceEffect); } this.sideConditions[status.id] = {id: status.id}; this.sideConditions[status.id].target = this; if (source) { this.sideConditions[status.id].source = source; this.sideConditions[status.id].sourcePosition = source.position; } if (status.duration) { this.sideConditions[status.id].duration = status.duration; } if (status.durationCallback) { this.sideConditions[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect); } if (!this.battle.singleEvent('Start', status, this.sideConditions[status.id], this, source, sourceEffect)) { delete this.sideConditions[status.id]; return false; } return true; } getSideCondition(status) { status = this.battle.getEffect(status); if (!this.sideConditions[status.id]) return null; return status; } removeSideCondition(status) { status = this.battle.getEffect(status); if (!this.sideConditions[status.id]) return false; this.battle.singleEvent('End', status, this.sideConditions[status.id], this); delete this.sideConditions[status.id]; return true; } send(...parts) { let sideUpdate = '|' + parts.map(part => { if (typeof part !== 'function') return part; return part(this); }).join('|'); this.battle.send('sideupdate', `${this.id}\n${sideUpdate}`); } emitCallback(...args) { this.battle.send('sideupdate', `${this.id}\n|callback|${args.join('|')}`); } emitRequest(update) { this.battle.send('request', `${this.id}\n${this.battle.rqid}\n${JSON.stringify(update)}`); } updateChoice() { const offset = this.choiceData.choices.length; this.battle.send('choice', this.id + "\n" + this.battle.rqid + "\n" + offset + "\n" + JSON.stringify({ done: this.choiceData.choices.map((choice, index) => choice === 'skip' ? '' : '' + index).join(""), leave: Array.from(this.choiceData.leaveIndices).join(""), enter: Array.from(this.choiceData.enterIndices).join(""), team: this.currentRequest === 'teampreview' ? this.choiceData.decisions.map(decision => decision.pokemon.position + 1).join("") : null, })); } getDecisionsFinished() { if (this.choiceData.decisions === true) return true; if (this.choiceData.skipsIndices.size) return false; if (this.currentRequest === 'teampreview') return this.choiceData.choices.length >= this.pokemon.length; return this.choiceData.choices.length >= this.active.length; } chooseMove(data, targetLoc, megaOrZ, dontPlay) { if (megaOrZ === true) megaOrZ = 'mega'; if (!targetLoc) targetLoc = 0; const activePokemon = this.active[this.choiceData.choices.length]; /** * Parse the move identifier (name or index), according to the request sent to the client. * If the move is not found, the decision is invalid without requiring further inspection. */ const requestMoves = activePokemon.getRequestData().moves; let moveid = data; let targetType = ''; if (typeof data === 'number' || /^[0-9]+$/.test(data)) { // Parse a one-based move index. const moveIndex = +data - 1; if (moveIndex < 0 || moveIndex >= requestMoves.length || !requestMoves[moveIndex]) { this.battle.debug("Can't use an unexpected move"); return false; } moveid = requestMoves[moveIndex].id; targetType = requestMoves[moveIndex].target; } else { // Parse a move ID. // Move names are also allowed, but may cause ambiguity (see client issue #167). moveid = toId(data); if (moveid.startsWith('hiddenpower')) { moveid = 'hiddenpower'; } for (let i = 0; i < requestMoves.length; i++) { if (requestMoves[i].id !== moveid) continue; targetType = requestMoves[i].target; break; } if (!targetType) { this.battle.debug("Can't use an unexpected move"); return false; } } let move = this.battle.getMove(moveid); let zMove = megaOrZ === 'zmove' ? this.battle.getZMove(move, activePokemon) : undefined; if (megaOrZ === 'zmove') { if (!zMove || this.choiceData.zmove) { this.emitCallback('cantz', activePokemon); // TODO: The client shouldn't have sent this request in the first place. this.battle.debug(`Can't use an unexpected z-move`); return false; } targetType = this.battle.getMove(zMove).target; if (!targetLoc && this.active.length >= 2 && this.battle.targetTypeChoices(targetType)) { // Compatibility fix: // Clients failed to select a target for Z-Moves based on spread moves // in the early stages of Gen 7 implementation. targetLoc = 1; } } /** * Validate targetting */ if (this.battle.targetTypeChoices(targetType)) { if (!targetLoc && this.active.length >= 2) { this.battle.debug("Can't use the move without a target"); return false; } } else { if (targetLoc) { this.battle.debug("Can't specify a target for the move"); return false; } } if (targetLoc && !this.battle.validTargetLoc(targetLoc, activePokemon, targetType)) { this.battle.debug("Invalid target selected for the move"); return false; } /** * Check whether the chosen move is really valid, accounting for * effects active in battle which could be unknown for the client. * Note that a Z-Move cannot be disabled. * * Upon reaching this stage, if a new decision is required, a server * reply is mandatory. */ const moves = activePokemon.getMoves(); if (!moves.length && !zMove) { // Override decision and use Struggle if there are no enabled moves with PP // Gen 4 and earlier announce a Pokemon has no moves left before the turn begins, and only to that player's side. if (this.gen <= 4) this.send('-activate', activePokemon, 'move: Struggle'); moveid = 'struggle'; } else if (!zMove) { // At least a move is valid. Check if the chosen one is. // This may include Struggle in Hackmons. let isEnabled = false; let disabledSource = ''; for (let i = 0; i < moves.length; i++) { if (moves[i].id !== moveid) continue; if (!moves[i].disabled) { isEnabled = true; break; } else if (moves[i].disabledSource) { disabledSource = moves[i].disabledSource; } } if (!isEnabled) { // Request a different choice this.emitCallback('cant', activePokemon, disabledSource, moveid); return false; } // The chosen move is valid yay } const decision = []; if (megaOrZ === 'mega') { if (!activePokemon.canMegaEvo || this.choiceData.mega) { this.emitCallback('cantmega', activePokemon); // TODO: The client shouldn't have sent this request in the first place. this.battle.debug(`Can't issue more than one Mega-Evolution command`); return false; } // TODO: Check that the Pokémon is not affected by Sky Drop. // (This is currently being done in `runMegaEvo`). decision.push({ choice: 'megaEvo', pokemon: activePokemon, }); } decision.push({ choice: 'move', pokemon: activePokemon, targetLoc: targetLoc, move: moveid, mega: megaOrZ === 'mega', zmove: zMove, }); this.choiceData.choices.push('move ' + moveid + (targetLoc ? ' ' + targetLoc : '') + (megaOrZ ? ' ' + megaOrZ : '')); this.choiceData.decisions.push(decision); if (activePokemon.maybeDisabled) { this.choiceData.finalDecision = this.choiceData.finalDecision || activePokemon.isLastActive(); } if (megaOrZ) { if (megaOrZ === 'mega') this.choiceData.mega = (this.choiceData.mega || 0) + 1; if (megaOrZ === 'zmove') this.choiceData.zmove = (this.choiceData.zmove || 0) + 1; } if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } chooseSwitch(data, index, dontPlay) { const slot = +data - 1; if (slot >= this.pokemon.length || !this.pokemon[slot]) { this.battle.debug("Can't switch: You can't switch to a Pokémon that doesn't exist"); return false; } if (slot < this.active.length) { this.battle.debug("Can't switch: You can't switch to an active Pokémon"); return false; } if (this.pokemon[slot].fainted) { this.battle.debug("Can't switch: You can't switch to a fainted Pokémon"); return false; } if (this.choiceData.enterIndices.has(slot)) { this.battle.debug("Can't switch: You can't switch to a Pokémon already queued to be switched"); return false; } if (typeof index === 'undefined') index = this.choiceData.choices.length; if (index < this.choiceData.choices.length && this.choiceData.choices[index] !== 'skip') { this.battle.debug("Can't switch: You can't override the decision for a Pokémon."); return false; } const activePokemon = this.active[index]; if (this.currentRequest === 'move') { if (activePokemon.trapped) { this.battle.debug("Can't switch: The active Pokémon is trapped"); this.emitCallback('trapped', activePokemon.position); return false; } else if (activePokemon.maybeTrapped) { this.choiceData.finalDecision = this.choiceData.finalDecision || activePokemon.isLastActive(); } } else if (this.currentRequest === 'switch') { if (this.choiceData.switchCounters.switch[0] >= this.choiceData.switchCounters.switch[1]) { this.battle.debug("Can't switch: You can't switch in more Pokémon than there are available."); return false; } this.choiceData.switchCounters.switch[0]++; } this.choiceData.enterIndices.add(slot); this.choiceData.leaveIndices.add(activePokemon.position); this.choiceData.choices[index] = 'switch ' + data; this.choiceData.decisions[index] = { choice: (this.currentRequest === 'switch' ? 'instaswitch' : 'switch'), pokemon: activePokemon, target: this.pokemon[slot], }; this.choiceData.skipsIndices.delete(index); if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } chooseTeam(data, dontPlay) { const positions = ('' + data).split('').map(datum => +datum - 1); for (const pos of positions) { const choiceOffset = this.choiceData.choices.length; if (pos >= this.pokemon.length) return false; if (this.choiceData.decisions.some(decision => decision.pokemon.position === pos)) return false; this.choiceData.choices.push('team ' + (pos + 1)); this.choiceData.decisions.push({ choice: 'team', side: this, index: choiceOffset, pokemon: this.pokemon[pos], priority: -choiceOffset, }); } if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } chooseShift(dontPlay) { const activePokemon = this.active[this.choiceData.choices.length]; this.choiceData.choices.push('shift'); this.choiceData.decisions.push({ choice: 'shift', pokemon: activePokemon, }); if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } undoChoices(count, nextIndex) { if (count === 0 || !this.choiceData.choices.length) return null; if (count === true) count = this.choiceData.choices.length; if (nextIndex === undefined) nextIndex = this.choiceData.choices.length; if (count > nextIndex) return null; for (let i = 1; i <= count; i++) { this.undoChoice(nextIndex - i); } } undoChoice(index) { const decision = this.choiceData.decisions.splice(index, 1)[0]; if (Array.isArray(decision) && decision[decision.length - 1].choice === 'move') { const moveDecision = decision[decision.length - 1]; // It probably doesn't make sense anymore to have a separate `mega` choice in the decider... if (moveDecision.mega) this.choiceData.mega--; if (moveDecision.zmove) this.choiceData.zmove--; } if (decision.choice === 'switch' || decision.choice === 'instaswitch') { this.choiceData.enterIndices.delete(decision.target.position); this.choiceData.leaveIndices.delete(this.choiceData.decisions.length); if (this.currentRequest === 'switch') { this.choiceData.switchCounters.switch[0]--; } } else if (decision.choice === 'pass') { if (this.currentRequest === 'switch' && decision.pokemon && decision.pokemon.switchFlag) { this.choiceData.switchCounters.pass[0]--; } } else if (this.currentRequest !== 'switch') { if (index !== this.choiceData.decisions.length) throw new Error(`Undoing decisions except the last during ${this.currentRequest} phase is unsupported (${index} !== ${this.choiceData.decisions.length})`); } this.choiceData.choices.splice(index, 1); } clearChoice() { const canSwitchOut = this.active.filter(pokemon => pokemon && pokemon.switchFlag).length; const canSwitchIn = this.pokemon.slice(this.active.length).filter(pokemon => pokemon && !pokemon.fainted).length; this.choiceData = { finalDecision: false, choices: [], decisions: [], switchCounters: { 'switch': [0, this.battle.currentRequest !== 'switch' ? 0 : Math.min(canSwitchOut, canSwitchIn)], 'pass': [0, this.battle.currentRequest !== 'switch' ? 0 : canSwitchOut - Math.min(canSwitchOut, canSwitchIn)], // When all the decisions go through, it should happen that // (switch|pass)Counter[0] === (switch|pass)Counter[1] // Otherwise, a player could illegally skip a turn. // TODO: Cover this in the test suite. }, enterIndices: new Set(), leaveIndices: new Set(), skipsIndices: new Set(), }; } // Special choices choosePass(index, dontPlay) { if (typeof index === 'undefined') index = this.choiceData.choices.length; if (index < this.choiceData.choices.length && this.choiceData.choices[index] !== 'skip') { this.battle.debug("Can't pass: You can't override the decision for a Pokémon."); return false; } const pokemon = this.active[index]; if (this.currentRequest === 'switch') { if (pokemon && pokemon.switchFlag) { // This condition will always happen if called by Battle#choose() if (this.choiceData.switchCounters.pass[0] >= this.choiceData.switchCounters.pass[1]) { this.battle.debug("Can't pass: You can't skip switching Pokémon in needlessly."); return false; } this.choiceData.switchCounters.pass[0]++; } } this.choiceData.choices[index] = 'pass'; this.choiceData.decisions[index] = { choice: 'pass', priority: 102, pokemon: pokemon, }; this.choiceData.skipsIndices.delete(this.choiceData.choices.length); if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } chooseSkip(index) { if (this.currentRequest !== 'switch') { this.battle.debug("Can't skip a Pokémon action"); return false; } if (this.active.pokemon.filter(pokemon => pokemon && pokemon.switchFlag).length <= 1) { this.battle.debug("Can't skip a Pokémon action except in a multiple K.O."); return false; } if (index !== this.choiceData.choices.length) { return this; } this.choiceData.choices.push('skip'); this.choiceData.decisions.push({ // Should never hit the battle queue choice: 'skip', pokemon: this.active[this.choiceData.choices.length], }); this.choiceData.skipsIndices.add(this.choiceData.choices.length); return this; } chooseDefault(dontPlay) { const choiceOffset = this.choiceData.choices.length; if (this.currentRequest === 'teampreview') { const chosenPositions = new Set(this.choiceData.decisions.map(decision => decision.pokemon.position)); const leftPositions = '012345'.slice(0, this.pokemon.length).split('').filter(pos => !chosenPositions.has(pos)); for (let i = 0; i < leftPositions.length; i++) { const pos = leftPositions[i]; this.choiceData.choices.push('team ' + (pos + 1)); this.choiceData.decisions.push({ choice: 'team', side: this, pokemon: this.pokemon[pos], index: choiceOffset + i, priority: -choiceOffset - i, }); } } else if (this.currentRequest === 'switch') { this.choiceData.choices.length = this.active.length; this.choiceData.decisions.length = this.active.length; const leaveIndices = []; const enterIndices = []; for (let i = 0; i < this.pokemon.length; i++) { if (!this.pokemon[i]) continue; if (i < this.choiceData.choices.length && this.choiceData.choices[i] && this.choiceData.choices[i] !== 'skip') { // Already decided continue; } if (i < this.active.length) { if (!this.pokemon[i].switchFlag || this.choiceData.leaveIndices.has(i)) continue; leaveIndices.push(i); this.choiceData.leaveIndices.add(i); } else { if (this.pokemon[i].fainted || this.choiceData.enterIndices.has(i)) continue; enterIndices.push(i); this.choiceData.enterIndices.add(i); } } // Some Pokémon might not have available substitutes. const willPass = leaveIndices.splice(Math.min(leaveIndices.length, enterIndices.length)); for (let i = 0; i < leaveIndices.length; i++) { this.choiceData.decisions[leaveIndices[i]] = { choice: this.foe.currentRequest === 'switch' ? 'instaswitch' : 'switch', pokemon: this.active[leaveIndices[i]], target: this.pokemon[enterIndices[i]], }; this.choiceData.choices[leaveIndices[i]] = 'switch ' + (enterIndices[i] + 1); } for (let i = 0; i < willPass.length; i++) { this.choiceData.decisions[willPass[i]] = { choice: 'pass', pokemon: this.active[willPass[i]], priority: 102, }; this.choiceData.choices[willPass[i]] = 'pass'; } } else if (this.currentRequest === 'move') { for (let i = choiceOffset; i < this.active.length; i++) { let pokemon = this.active[i]; if (!pokemon || pokemon.fainted) continue; let lockedMove = pokemon.getLockedMove(); if (lockedMove) { const lockedMoveTarget = this.battle.runEvent('LockMoveTarget', pokemon); this.choiceData.choices.push('move ' + lockedMove + (typeof lockedMoveTarget === 'number' ? ' ' + lockedMoveTarget : '')); this.choiceData.decisions.push({ choice: 'move', pokemon: pokemon, targetLoc: lockedMoveTarget || 0, move: lockedMove, }); continue; } let moveid = 'struggle'; let moves = pokemon.getMoves(); for (let j = 0; j < moves.length; j++) { if (moves[j].disabled) continue; moveid = moves[j].id; break; } this.choiceData.choices.push('move ' + moveid + ' 0'); this.choiceData.decisions.push({ choice: 'move', pokemon: pokemon, targetLoc: 0, move: moveid, }); } } if (!dontPlay && !this.battle.checkDecisions()) return this; // allow chaining return true; } resolveDecision() { if (this.currentRequest && !this.getDecisionsFinished()) { this.chooseDefault(true); this.choiceData.choices = []; } else if (this.choiceData.decisions === true) { this.choiceData.choices = []; } } destroy() { // deallocate ourself // deallocate children and get rid of references to them for (let i = 0; i < this.pokemon.length; i++) { if (this.pokemon[i]) this.pokemon[i].destroy(); this.pokemon[i] = null; } this.pokemon = null; for (let i = 0; i < this.active.length; i++) { this.active[i] = null; } this.active = null; if (this.choiceData.decisions && this.choiceData.decisions !== true) { this.choiceData.decisions.forEach(decision => { delete decision.side; delete decision.pokemon; delete decision.target; }); } this.choiceData = null; // get rid of some possibly-circular references this.battle = null; this.foe = null; } } class Battle extends Tools.BattleDex { /** * Initialises a Battle. * * @param {PRNG} [maybePrng] */ init(roomid, format, rated, send, maybePrng) { this.log = []; this.sides = [null, null]; this.roomid = roomid; this.id = roomid; this.rated = rated; this.weatherData = {id:''}; this.terrainData = {id:''}; this.pseudoWeather = {}; this.format = toId(format); this.formatData = {id:this.format}; this.effect = {id:''}; this.effectData = {id:''}; this.event = {id:''}; this.gameType = (format.gameType || 'singles'); this.reportExactHP = !!format.debug; this.replayExactHP = !format.team; this.queue = []; this.faintQueue = []; this.messageLog = []; this.send = send || (() => {}); this.turn = 0; this.p1 = null; this.p2 = null; this.lastUpdate = 0; this.weather = ''; this.terrain = ''; this.ended = false; this.started = false; this.active = false; this.eventDepth = 0; this.lastMove = ''; this.activeMove = null; this.activePokemon = null; this.activeTarget = null; this.midTurn = false; this.currentRequest = ''; this.currentRequestDetails = ''; this.rqid = 0; this.lastMoveLine = 0; this.reportPercentages = false; this.supportCancel = false; this.events = null; this.abilityOrder = 0; this.prng = maybePrng || new PRNG(); this.prngSeed = this.prng.startingSeed.slice(); } static logReplay(data, isReplay) { if (isReplay === true) return data; return ''; } toString() { return 'Battle: ' + this.format; } random(m, n) { return this.prng.next(m, n); } setWeather(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) { if (sourceEffect && sourceEffect.effectType === 'Ability') { if (this.gen > 5 || this.weatherData.duration === 0) { return false; } } else if (this.gen > 2 || status.id === 'sandstorm') { return false; } } if (status.id) { let result = this.runEvent('SetWeather', source, source, status); if (!result) { if (result === false) { if (sourceEffect && sourceEffect.weather) { this.add('-fail', source, sourceEffect, '[from]: ' + this.weather); } else if (sourceEffect && sourceEffect.effectType === 'Ability') { this.add('-ability', source, sourceEffect, '[from] ' + this.weather, '[fail]'); } } return null; } } if (this.weather && !status.id) { let oldstatus = this.getWeather(); this.singleEvent('End', oldstatus, this.weatherData, this); } let prevWeather = this.weather; let 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; } return true; } clearWeather() { return this.setWeather(''); } effectiveWeather(target) { if (this.event) { if (!target) target = this.event.target; } if (this.suppressingWeather()) return ''; return this.weather; } isWeather(weather, target) { let ourWeather = this.effectiveWeather(target); if (!Array.isArray(weather)) { return ourWeather === toId(weather); } return weather.map(toId).includes(ourWeather); } getWeather() { return this.getEffect(this.weather); } setTerrain(status, source, sourceEffect) { status = this.getEffect(status); if (sourceEffect === undefined && this.effect) sourceEffect = this.effect; if (source === undefined && this.event && this.event.target) source = this.event.target; if (this.terrain === status.id) return false; if (this.terrain && !status.id) { let oldstatus = this.getTerrain(); this.singleEvent('End', oldstatus, this.terrainData, this); } let prevTerrain = this.terrain; let prevTerrainData = this.terrainData; this.terrain = status.id; this.terrainData = {id: status.id}; if (source) { this.terrainData.source = source; this.terrainData.sourcePosition = source.position; } if (status.duration) { this.terrainData.duration = status.duration; } if (status.durationCallback) { this.terrainData.duration = status.durationCallback.call(this, source, sourceEffect); } if (!this.singleEvent('Start', status, this.terrainData, this, source, sourceEffect)) { this.terrain = prevTerrain; this.terrainData = prevTerrainData; return false; } return true; } clearTerrain() { return this.setTerrain(''); } effectiveTerrain(target) { if (this.event) { if (!target) target = this.event.target; } if (!this.runEvent('TryTerrain', target)) return ''; return this.terrain; } isTerrain(terrain, target) { let ourTerrain = this.effectiveTerrain(target); if (!Array.isArray(terrain)) { return ourTerrain === toId(terrain); } return terrain.map(toId).includes(ourTerrain); } getTerrain() { return this.getEffect(this.terrain); } getFormat() { return this.getEffect(this.format); } addPseudoWeather(status, source, sourceEffect) { status = this.getEffect(status); if (this.pseudoWeather[status.id]) { if (!status.onRestart) return false; return this.singleEvent('Restart', status, this.pseudoWeather[status.id], this, source, sourceEffect); } this.pseudoWeather[status.id] = {id: status.id}; if (source) { this.pseudoWeather[status.id].source = source; this.pseudoWeather[status.id].sourcePosition = source.position; } if (status.duration) { this.pseudoWeather[status.id].duration = status.duration; } if (status.durationCallback) { this.pseudoWeather[status.id].duration = status.durationCallback.call(this, source, sourceEffect); } if (!this.singleEvent('Start', status, this.pseudoWeather[status.id], this, source, sourceEffect)) { delete this.pseudoWeather[status.id]; return false; } return true; } getPseudoWeather(status) { status = this.getEffect(status); if (!this.pseudoWeather[status.id]) return null; return status; } removePseudoWeather(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]; return true; } suppressingAttackEvents() { return this.activePokemon && this.activePokemon.isActive && this.activeMove && this.activeMove.ignoreAbility; } suppressingWeather() { let pokemon; for (let i = 0; i < this.sides.length; i++) { for (let j = 0; j < this.sides[i].active.length; j++) { pokemon = this.sides[i].active[j]; if (pokemon && !pokemon.ignoringAbility() && pokemon.getAbility().suppressWeather) { return true; } } } return false; } setActiveMove(move, pokemon, target) { if (!move) move = null; if (!pokemon) pokemon = null; if (!target) target = pokemon; this.activeMove = move; this.activePokemon = pokemon; this.activeTarget = target; } clearActiveMove(failed) { if (this.activeMove) { if (!failed) { this.lastMove = this.activeMove.id; } this.activeMove = null; this.activePokemon = null; this.activeTarget = null; } } updateSpeed() { let actives = this.p1.active; for (let i = 0; i < actives.length; i++) { if (actives[i]) actives[i].updateSpeed(); } actives = this.p2.active; for (let i = 0; i < actives.length; i++) { if (actives[i]) actives[i].updateSpeed(); } } static comparePriority(a, b) { a.priority = a.priority || 0; a.subPriority = a.subPriority || 0; a.speed = a.speed || 0; b.priority = b.priority || 0; b.subPriority = b.subPriority || 0; b.speed = b.speed || 0; if ((typeof a.order === 'number' || typeof b.order === 'number') && a.order !== b.order) { if (typeof a.order !== 'number') { return -1; } if (typeof b.order !== 'number') { return 1; } if (b.order - a.order) { return -(b.order - a.order); } } if (b.priority - a.priority) { return b.priority - a.priority; } if (b.speed - a.speed) { return b.speed - a.speed; } if (b.subOrder - a.subOrder) { return -(b.subOrder - a.subOrder); } return Math.random() - 0.5; } static compareRedirectOrder(a, b) { a.priority = a.priority || 0; a.speed = a.speed || 0; b.priority = b.priority || 0; b.speed = b.speed || 0; if (b.priority - a.priority) { return b.priority - a.priority; } if (b.speed - a.speed) { return b.speed - a.speed; } if (b.thing.abilityOrder - a.thing.abilityOrder) { return -(b.thing.abilityOrder - a.thing.abilityOrder); } return 0; } getResidualStatuses(thing, callbackType) { let statuses = this.getRelevantEffectsInner(thing || this, callbackType || 'residualCallback', null, null, false, true, 'duration'); statuses.sort(Battle.comparePriority); //if (statuses[0]) this.debug('match ' + (callbackType || 'residualCallback') + ': ' + statuses[0].status.id); return statuses; } eachEvent(eventid, effect, relayVar) { let actives = []; if (!effect && this.effect) effect = this.effect; for (let i = 0; i < this.sides.length; i++) { let side = this.sides[i]; for (let j = 0; j < side.active.length; j++) { if (side.active[j]) actives.push(side.active[j]); } } actives.sort((a, b) => { if (b.speed - a.speed) { return b.speed - a.speed; } return Math.random() - 0.5; }); for (let i = 0; i < actives.length; i++) { this.runEvent(eventid, actives[i], null, effect, relayVar); } } residualEvent(eventid, relayVar) { let statuses = this.getRelevantEffectsInner(this, 'on' + eventid, null, null, false, true, 'duration'); statuses.sort(Battle.comparePriority); while (statuses.length) { let statusObj = statuses.shift(); let status = statusObj.status; if (statusObj.thing.fainted) continue; if (statusObj.statusData && statusObj.statusData.duration) { statusObj.statusData.duration--; if (!statusObj.statusData.duration) { statusObj.end.call(statusObj.thing, status.id); continue; } } this.singleEvent(eventid, status, statusObj.statusData, statusObj.thing, relayVar); } } // The entire event system revolves around this function // (and its helper functions, getRelevant * ) singleEvent(eventid, effect, effectData, target, source, sourceEffect, relayVar) { if (this.eventDepth >= 8) { // oh fuck this.add('message', 'STACK LIMIT EXCEEDED'); this.add('message', 'PLEASE REPORT IN BUG THREAD'); this.add('message', 'Event: ' + eventid); this.add('message', 'Parent event: ' + this.event.id); throw new Error("Stack overflow"); } //this.add('Event: ' + eventid + ' (depth ' + this.eventDepth + ')'); effect = this.getEffect(effect); let hasRelayVar = true; if (relayVar === undefined) { relayVar = true; hasRelayVar = false; } if (effect.effectType === 'Status' && target.status !== effect.id) { // it's changed; call it off return relayVar; } if (eventid !== 'Start' && eventid !== 'TakeItem' && eventid !== 'Primal' && effect.effectType === 'Item' && (target instanceof BattlePokemon) && target.ignoringItem()) { this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room'); return relayVar; } if (eventid !== 'End' && effect.effectType === 'Ability' && (target instanceof BattlePokemon) && target.ignoringAbility()) { this.debug(eventid + ' handler suppressed by Gastro Acid'); return relayVar; } if (effect.effectType === 'Weather' && eventid !== 'Start' && eventid !== 'Residual' && eventid !== 'End' && this.suppressingWeather()) { this.debug(eventid + ' handler suppressed by Air Lock'); return relayVar; } if (effect['on' + eventid] === undefined) return relayVar; let parentEffect = this.effect; let parentEffectData = this.effectData; let parentEvent = this.event; this.effect = effect; this.effectData = effectData; this.event = {id: eventid, target: target, source: source, effect: sourceEffect}; this.eventDepth++; let args = [target, source, sourceEffect]; if (hasRelayVar) args.unshift(relayVar); let 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. */ runEvent(eventid, target, source, effect, relayVar, onEffect, fastExit) { // if (Battle.eventCounter) { // if (!Battle.eventCounter[eventid]) Battle.eventCounter[eventid] = 0; // Battle.eventCounter[eventid]++; // } if (this.eventDepth >= 8) { // oh fuck this.add('message', 'STACK LIMIT EXCEEDED'); this.add('message', 'PLEASE REPORT IN BUG THREAD'); this.add('message', 'Event: ' + eventid); this.add('message', 'Parent event: ' + this.event.id); throw new Error("Stack overflow"); } if (!target) target = this; let statuses = this.getRelevantEffects(target, 'on' + eventid, 'onSource' + eventid, source); if (fastExit) { statuses.sort(Battle.compareRedirectOrder); } else { statuses.sort(Battle.comparePriority); } let hasRelayVar = true; effect = this.getEffect(effect); let args = [target, source, effect]; //console.log('Event: ' + eventid + ' (depth ' + this.eventDepth + ') t:' + target.id + ' s:' + (!source || source.id) + ' e:' + effect.id); if (relayVar === undefined || relayVar === null) { relayVar = true; hasRelayVar = false; } else { args.unshift(relayVar); } let parentEvent = this.event; this.event = {id: eventid, target: target, source: source, effect: effect, modifier: 1}; this.eventDepth++; if (onEffect && 'on' + eventid in effect) { statuses.unshift({status: effect, callback: effect['on' + eventid], statusData: {}, end: null, thing: target}); } for (let i = 0; i < statuses.length; i++) { let status = statuses[i].status; let thing = statuses[i].thing; //this.debug('match ' + eventid + ': ' + status.id + ' ' + status.effectType); if (status.effectType === 'Status' && thing.status !== status.id) { // it's changed; call it off continue; } if (status.effectType === 'Ability' && !status.isUnbreakable && this.suppressingAttackEvents() && this.activePokemon !== thing) { // ignore attacking events let AttackingEvents = { BeforeMove: 1, BasePower: 1, Immunity: 1, RedirectTarget: 1, Heal: 1, SetStatus: 1, CriticalHit: 1, ModifyAtk: 1, ModifyDef: 1, ModifySpA: 1, ModifySpD: 1, ModifySpe: 1, ModifyAccuracy: 1, ModifyBoost: 1, ModifyDamage: 1, ModifySecondaries: 1, ModifyWeight: 1, TryAddVolatile: 1, TryHit: 1, TryHitSide: 1, TryMove: 1, Boost: 1, DragOut: 1, Effectiveness: 1, }; if (eventid in AttackingEvents) { this.debug(eventid + ' handler suppressed by Mold Breaker'); continue; } else if (eventid === 'Damage' && effect && effect.effectType === 'Move') { this.debug(eventid + ' handler suppressed by Mold Breaker'); continue; } } if (eventid !== 'Start' && eventid !== 'SwitchIn' && eventid !== 'TakeItem' && status.effectType === 'Item' && (thing instanceof BattlePokemon) && thing.ignoringItem()) { if (eventid !== 'Update') { this.debug(eventid + ' handler suppressed by Embargo, Klutz or Magic Room'); } continue; } else if (eventid !== 'End' && status.effectType === 'Ability' && (thing instanceof BattlePokemon) && thing.ignoringAbility()) { if (eventid !== 'Update') { this.debug(eventid + ' handler suppressed by Gastro Acid'); } continue; } if ((status.effectType === 'Weather' || eventid === 'Weather') && eventid !== 'Residual' && eventid !== 'End' && this.suppressingWeather()) { this.debug(eventid + ' handler suppressed by Air Lock'); continue; } let returnVal; if (typeof statuses[i].callback === 'function') { let parentEffect = this.effect; let parentEffectData = this.effectData; this.effect = statuses[i].status; this.effectData = statuses[i].statusData; this.effectData.target = thing; returnVal = statuses[i].callback.apply(this, args); this.effect = parentEffect; this.effectData = parentEffectData; } else { returnVal = statuses[i].callback; } if (returnVal !== undefined) { relayVar = returnVal; if (!relayVar || fastExit) break; if (hasRelayVar) { args[0] = relayVar; } } } this.eventDepth--; if (this.event.modifier !== 1 && typeof relayVar === 'number') { // this.debug(eventid + ' modifier: 0x' + ('0000' + (this.event.modifier * 4096).toString(16)).slice(-4).toUpperCase()); relayVar = this.modify(relayVar, this.event.modifier); } this.event = parentEvent; return relayVar; } /** * priorityEvent works just like runEvent, except it exits and returns * on the first non-undefined value instead of only on null/false. */ priorityEvent(eventid, target, source, effect, relayVar, onEffect) { return this.runEvent(eventid, target, source, effect, relayVar, onEffect, true); } resolveLastPriority(statuses, callbackType) { let order = false; let priority = 0; let subOrder = 0; let status = statuses[statuses.length - 1]; if (status.status[callbackType + 'Order']) { order = status.status[callbackType + 'Order']; } if (status.status[callbackType + 'Priority']) { priority = status.status[callbackType + 'Priority']; } else if (status.status[callbackType + 'SubOrder']) { subOrder = status.status[callbackType + 'SubOrder']; } status.order = order; status.priority = priority; status.subOrder = subOrder; if (status.thing && status.thing.getStat) status.speed = status.thing.speed; } // bubbles up to parents getRelevantEffects(thing, callbackType, foeCallbackType, foeThing) { let statuses = this.getRelevantEffectsInner(thing, callbackType, foeCallbackType, foeThing, true, false); //if (statuses[0]) this.debug('match ' + callbackType + ': ' + statuses[0].status.id); return statuses; } getRelevantEffectsInner(thing, callbackType, foeCallbackType, foeThing, bubbleUp, bubbleDown, getAll) { if (!callbackType || !thing) return []; let statuses = []; let status; if (thing.sides) { for (let i in this.pseudoWeather) { status = this.getPseudoWeather(i); if (status[callbackType] !== undefined || (getAll && thing.pseudoWeather[i][getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: this.pseudoWeather[i], end: this.removePseudoWeather, thing: thing}); this.resolveLastPriority(statuses, callbackType); } } status = this.getWeather(); if (status[callbackType] !== undefined || (getAll && thing.weatherData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: this.weatherData, end: this.clearWeather, thing: thing, priority: status[callbackType + 'Priority'] || 0}); this.resolveLastPriority(statuses, callbackType); } status = this.getTerrain(); if (status[callbackType] !== undefined || (getAll && thing.terrainData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: this.terrainData, end: this.clearTerrain, thing: thing, priority: status[callbackType + 'Priority'] || 0}); this.resolveLastPriority(statuses, callbackType); } status = this.getFormat(); if (status[callbackType] !== undefined || (getAll && thing.formatData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: this.formatData, end: function () {}, thing: thing, priority: status[callbackType + 'Priority'] || 0}); this.resolveLastPriority(statuses, callbackType); } if (this.events && this.events[callbackType] !== undefined) { for (let i = 0; i < this.events[callbackType].length; i++) { let handler = this.events[callbackType][i]; let statusData; switch (handler.target.effectType) { case 'Format': statusData = this.formatData; } statuses.push({status: handler.target, callback: handler.callback, statusData: statusData, end: function () {}, thing: thing, priority: handler.priority, order: handler.order, subOrder: handler.subOrder}); } } if (bubbleDown) { statuses = statuses.concat(this.getRelevantEffectsInner(this.p1, callbackType, null, null, false, true, getAll)); statuses = statuses.concat(this.getRelevantEffectsInner(this.p2, callbackType, null, null, false, true, getAll)); } return statuses; } if (thing.pokemon) { for (let i in thing.sideConditions) { status = thing.getSideCondition(i); if (status[callbackType] !== undefined || (getAll && thing.sideConditions[i][getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: thing.sideConditions[i], end: thing.removeSideCondition, thing: thing}); this.resolveLastPriority(statuses, callbackType); } } if (foeCallbackType) { statuses = statuses.concat(this.getRelevantEffectsInner(thing.foe, foeCallbackType, null, null, false, false, getAll)); if (foeCallbackType.substr(0, 5) === 'onFoe') { let eventName = foeCallbackType.substr(5); statuses = statuses.concat(this.getRelevantEffectsInner(thing.foe, 'onAny' + eventName, null, null, false, false, getAll)); statuses = statuses.concat(this.getRelevantEffectsInner(thing, 'onAny' + eventName, null, null, false, false, getAll)); } } if (bubbleUp) { statuses = statuses.concat(this.getRelevantEffectsInner(this, callbackType, null, null, true, false, getAll)); } if (bubbleDown) { for (let i = 0; i < thing.active.length; i++) { statuses = statuses.concat(this.getRelevantEffectsInner(thing.active[i], callbackType, null, null, false, true, getAll)); } } return statuses; } if (!thing.getStatus) { //this.debug(JSON.stringify(thing)); return statuses; } status = thing.getStatus(); if (status[callbackType] !== undefined || (getAll && thing.statusData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: thing.statusData, end: thing.clearStatus, thing: thing}); this.resolveLastPriority(statuses, callbackType); } for (let i in thing.volatiles) { status = thing.getVolatile(i); if (status[callbackType] !== undefined || (getAll && thing.volatiles[i][getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: thing.volatiles[i], end: thing.removeVolatile, thing: thing}); this.resolveLastPriority(statuses, callbackType); } } status = thing.getAbility(); if (status[callbackType] !== undefined || (getAll && thing.abilityData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: thing.abilityData, end: thing.clearAbility, thing: thing}); this.resolveLastPriority(statuses, callbackType); } status = thing.getItem(); if (status[callbackType] !== undefined || (getAll && thing.itemData[getAll])) { statuses.push({status: status, callback: status[callbackType], statusData: thing.itemData, end: thing.clearItem, thing: thing}); this.resolveLastPriority(statuses, callbackType); } status = this.getEffect(thing.template.baseSpecies); if (status[callbackType] !== undefined) { statuses.push({status: status, callback: status[callbackType], statusData: thing.speciesData, end: function () {}, thing: thing}); this.resolveLastPriority(statuses, callbackType); } if (foeThing && foeCallbackType && foeCallbackType.substr(0, 8) !== 'onSource') { statuses = statuses.concat(this.getRelevantEffectsInner(foeThing, foeCallbackType, null, null, false, false, getAll)); } else if (foeCallbackType) { let foeActive = thing.side.foe.active; let allyActive = thing.side.active; let eventName = ''; if (foeCallbackType.substr(0, 8) === 'onSource') { eventName = foeCallbackType.substr(8); if (foeThing) { statuses = statuses.concat(this.getRelevantEffectsInner(foeThing, foeCallbackType, null, null, false, false, getAll)); } foeCallbackType = 'onFoe' + eventName; foeThing = null; } if (foeCallbackType.substr(0, 5) === 'onFoe') { eventName = foeCallbackType.substr(5); for (let i = 0; i < allyActive.length; i++) { if (!allyActive[i] || allyActive[i].fainted) continue; statuses = statuses.concat(this.getRelevantEffectsInner(allyActive[i], 'onAlly' + eventName, null, null, false, false, getAll)); statuses = statuses.concat(this.getRelevantEffectsInner(allyActive[i], 'onAny' + eventName, null, null, false, false, getAll)); } for (let i = 0; i < foeActive.length; i++) { if (!foeActive[i] || foeActive[i].fainted) continue; statuses = statuses.concat(this.getRelevantEffectsInner(foeActive[i], 'onAny' + eventName, null, null, false, false, getAll)); } } for (let i = 0; i < foeActive.length; i++) { if (!foeActive[i] || foeActive[i].fainted) continue; statuses = statuses.concat(this.getRelevantEffectsInner(foeActive[i], foeCallbackType, null, null, false, false, getAll)); } } if (bubbleUp) { statuses = statuses.concat(this.getRelevantEffectsInner(thing.side, callbackType, foeCallbackType, null, true, false, getAll)); } return statuses; } /** * Use this function to attach custom event handlers to a battle. See Battle#runEvent for * more information on how to write callbacks for event handlers. * * Try to use this sparingly. Most event handlers can be simply placed in a format instead. * * this.on(eventid, target, callback) * will set the callback as an event handler for the target when eventid is called with the * default priority. Currently only valid formats are supported as targets but this will * eventually be expanded to support other target types. * * this.on(eventid, target, priority, callback) * will set the callback as an event handler for the target when eventid is called with the * provided priority. Priority can either be a number or an object that contains the priority, * order, and subOrder for the evend handler as needed (undefined keys will use default values) */ on(eventid, target, ...rest) { // rest = [priority, callback] if (!eventid) throw new TypeError("Event handlers must have an event to listen to"); if (!target) throw new TypeError("Event handlers must have a target"); if (!rest.length) throw new TypeError("Event handlers must have a callback"); if (target.effectType !== 'Format') { throw new TypeError(`${target.effectType} targets are not supported at this time`); } let callback, priority, order, subOrder, data; if (rest.length === 1) { [callback] = rest; priority = 0; order = false; subOrder = 0; } else { [data, callback] = rest; if (typeof data === 'object') { priority = data['priority'] || 0; order = data['order'] || false; subOrder = data['subOrder'] || 0; } else { priority = data || 0; order = false; subOrder = 0; } } let eventHandler = {callback, target, priority, order, subOrder}; let callbackType = `on${eventid}`; if (!this.events) this.events = {}; if (this.events[callbackType] === undefined) { this.events[callbackType] = [eventHandler]; } else { this.events[callbackType].push(eventHandler); } } getPokemon(id) { if (typeof id !== 'string') id = id.id; for (let i = 0; i < this.p1.pokemon.length; i++) { let pokemon = this.p1.pokemon[i]; if (pokemon.id === id) return pokemon; } for (let i = 0; i < this.p2.pokemon.length; i++) { let pokemon = this.p2.pokemon[i]; if (pokemon.id === id) return pokemon; } return null; } makeRequest(type, requestDetails) { if (type) { this.currentRequest = type; this.currentRequestDetails = requestDetails || ''; this.rqid++; this.p1.clearChoice(); this.p2.clearChoice(); } else { type = this.currentRequest; requestDetails = this.currentRequestDetails; } // default to no request let p1request = null; let p2request = null; this.p1.currentRequest = ''; this.p2.currentRequest = ''; let switchTable = []; switch (type) { case 'switch': { for (let i = 0, l = this.p1.active.length; i < l; i++) { let active = this.p1.active[i]; switchTable.push(!!(active && active.switchFlag)); } if (switchTable.some(flag => flag === true)) { this.p1.currentRequest = 'switch'; p1request = {forceSwitch: switchTable, side: this.p1.getData(), rqid: this.rqid}; } switchTable = []; for (let i = 0, l = this.p2.active.length; i < l; i++) { let active = this.p2.active[i]; switchTable.push(!!(active && active.switchFlag)); } if (switchTable.some(flag => flag === true)) { this.p2.currentRequest = 'switch'; p2request = {forceSwitch: switchTable, side: this.p2.getData(), rqid: this.rqid}; } break; } case 'teampreview': this.add('teampreview' + (requestDetails ? '|' + requestDetails : '')); this.p1.currentRequest = 'teampreview'; p1request = {teamPreview: true, side: this.p1.getData(), rqid: this.rqid}; this.p2.currentRequest = 'teampreview'; p2request = {teamPreview: true, side: this.p2.getData(), rqid: this.rqid}; break; default: { this.p1.currentRequest = 'move'; let activeData = this.p1.active.map(pokemon => pokemon && pokemon.getRequestData()); p1request = {active: activeData, side: this.p1.getData(), rqid: this.rqid}; this.p2.currentRequest = 'move'; activeData = this.p2.active.map(pokemon => pokemon && pokemon.getRequestData()); p2request = {active: activeData, side: this.p2.getData(), rqid: this.rqid}; break; } } if (this.p1 && this.p2) { let inactiveSide = -1; if (p1request && !p2request) { inactiveSide = 0; } else if (!p1request && p2request) { inactiveSide = 1; } if (inactiveSide !== this.inactiveSide) { this.send('inactiveside', inactiveSide); this.inactiveSide = inactiveSide; } } if (p1request) { if (!this.supportCancel || !p2request) p1request.noCancel = true; this.p1.emitRequest(p1request); } else { this.p1.choiceData.decisions = true; this.p1.emitRequest({wait: true, side: this.p1.getData()}); } if (p2request) { if (!this.supportCancel || !p1request) p2request.noCancel = true; this.p2.emitRequest(p2request); } else { this.p2.choiceData.decisions = true; this.p2.emitRequest({wait: true, side: this.p2.getData()}); } if (this.p1.getDecisionsFinished() && this.p2.getDecisionsFinished()) { if (this.p1.choiceData.decisions === true && this.p2.choiceData.decisions === true) { if (type !== 'move') { // TODO: investigate this race condition; should be fixed // properly later return this.makeRequest('move'); } this.add('html', '