/** * Simulator Pokemon * Pokemon Showdown - http://pokemonshowdown.com/ * * @license MIT license */ import { State } from './state'; import { toID } from './dex'; import type { DynamaxOptions, PokemonMoveRequestData, PokemonSwitchRequestData } from './side'; /** A Pokemon's move slot. */ interface MoveSlot { id: ID; move: string; pp: number; maxpp: number; target?: string; disabled: boolean | string; disabledSource?: string; used: boolean; virtual?: boolean; } interface Attacker { source: Pokemon; damage: number; thisTurn: boolean; move?: ID; slot: PokemonSlot; damageValue?: (number | boolean | undefined); } export interface EffectState { id: string; effectOrder: number; duration?: number; [k: string]: any; } // Berries which restore PP/HP and thus inflict external staleness when given to an opponent as // there are very few non-malicious competitive reasons to do so export const RESTORATIVE_BERRIES = new Set([ 'leppaberry', 'aguavberry', 'enigmaberry', 'figyberry', 'iapapaberry', 'magoberry', 'sitrusberry', 'wikiberry', 'oranberry', ] as ID[]); export class Pokemon { readonly side: Side; readonly battle: Battle; readonly set: PokemonSet; readonly name: string; /** `` `${sideid}: ${name}` `` - used to refer to pokemon in the protocol */ readonly fullname: string; readonly level: number; readonly gender: GenderName; readonly happiness: number; readonly pokeball: ID; readonly dynamaxLevel: number; readonly gigantamax: boolean; /** Transform keeps the original pre-transformed Hidden Power in Gen 2-4. */ readonly baseHpType: string; readonly baseHpPower: number; readonly baseMoveSlots: MoveSlot[]; moveSlots: MoveSlot[]; hpType: string; hpPower: number; /** * Index of `pokemon.side.pokemon` and `pokemon.side.active`, which are * guaranteed to be the same for active pokemon. Note that this isn't * its field position in multi battles - use `getSlot()` for that. */ position: number; /** * Information about this pokemon visible to opponents when in battle * (species, gender, level, shininess, tera state). * @see https://github.com/smogon/pokemon-showdown/blob/master/sim/SIM-PROTOCOL.md#identifying-pok%C3%A9mon */ details: string; baseSpecies: Species; species: Species; speciesState: EffectState; status: ID; statusState: EffectState; volatiles: { [id: string]: EffectState }; showCure?: boolean; /** * These are the basic stats that appear on the in-game stats screen: * calculated purely from the species base stats, level, IVs, EVs, * and Nature, before modifications from item, ability, etc. * * Forme changes affect these, but Transform doesn't. */ baseStoredStats: StatsTable; /** * These are pre-modification stored stats in-battle. At switch-in, * they're identical to `baseStoredStats`, but can be temporarily changed * until switch-out by effects such as Power Trick and Transform. * * Stat multipliers from abilities, items, and volatiles, such as * Solar Power, Choice Band, or Swords Dance, are not stored in * `storedStats`, but applied on top and accessed by `pokemon.getStat`. * * (Except in Gen 1, where stat multipliers are stored, leading * to several famous glitches.) */ storedStats: StatsExceptHPTable; boosts: BoostsTable; baseAbility: ID; ability: ID; abilityState: EffectState; item: ID; itemState: EffectState; lastItem: ID; usedItemThisTurn: boolean; ateBerry: boolean; trapped: boolean | "hidden"; maybeTrapped: boolean; maybeDisabled: boolean; illusion: Pokemon | null; transformed: boolean; maxhp: number; /** This is the max HP before Dynamaxing; it's updated for Power Construct etc */ baseMaxhp: number; hp: number; fainted: boolean; faintQueued: boolean; subFainted: boolean | null; /** If this Pokemon should revert to its set species when it faints */ regressionForme: boolean; types: string[]; addedType: string; knownType: boolean; /** Keeps track of what type the client sees for this Pokemon. */ apparentType: string; /** * If the switch is called by an effect with a special switch * message, like U-turn or Baton Pass, this will be the ID of * the calling effect. */ switchFlag: ID | boolean; forceSwitchFlag: boolean; skipBeforeSwitchOutEventFlag: boolean; draggedIn: number | null; newlySwitched: boolean; beingCalledBack: boolean; lastMove: ActiveMove | null; // Gen 2 only lastMoveEncore?: ActiveMove | null; lastMoveUsed: ActiveMove | null; lastMoveTargetLoc?: number; moveThisTurn: string | boolean; statsRaisedThisTurn: boolean; statsLoweredThisTurn: boolean; /** * The result of the last move used on the previous turn by this * Pokemon. Stomping Tantrum checks this property for a value of false * when determine whether to double its power, but it has four * possible values: * * undefined indicates this Pokemon was not active last turn. It should * not be used to indicate that a move was attempted and failed, either * in a way that boosts Stomping Tantrum or not. * * null indicates that the Pokemon's move was skipped in such a way * that does not boost Stomping Tantrum, either from having to recharge * or spending a turn trapped by another Pokemon's Sky Drop. * * false indicates that the move completely failed to execute for any * reason not mentioned above, including missing, the target being * immune, the user being immobilized by an effect such as paralysis, etc. * * true indicates that the move successfully executed one or more of * its effects on one or more targets, including hitting with an attack * but dealing 0 damage to the target in cases such as Disguise, or that * the move was blocked by one or more moves such as Protect. */ moveLastTurnResult: boolean | null | undefined; /** * The result of the most recent move used this turn by this Pokemon. * At the start of each turn, the value stored here is moved to its * counterpart, moveLastTurnResult, and this property is reinitialized * to undefined. This property can have one of four possible values: * * undefined indicates that this Pokemon has not yet finished an * attempt to use a move this turn. As this value is only overwritten * after a move finishes execution, it is not sufficient for an event * to examine only this property when checking if a Pokemon has not * moved yet this turn if the event could take place during that * Pokemon's move. * * null indicates that the Pokemon's move was skipped in such a way * that does not boost Stomping Tantrum, either from having to recharge * or spending a turn trapped by another Pokemon's Sky Drop. * * false indicates that the move completely failed to execute for any * reason not mentioned above, including missing, the target being * immune, the user being immobilized by an effect such as paralysis, etc. * * true indicates that the move successfully executed one or more of * its effects on one or more targets, including hitting with an attack * but dealing 0 damage to the target in cases such as Disguise. It can * also mean that the move was blocked by one or more moves such as * Protect. Uniquely, this value can also be true if this Pokemon mega * evolved or ultra bursted this turn, but in that case the value should * always be overwritten by a move action before the end of that turn. */ moveThisTurnResult: boolean | null | undefined; /** * The undynamaxed HP value this Pokemon was reduced to by damage this turn, * or false if it hasn't taken damage yet this turn * * Used for Assurance, Emergency Exit, and Wimp Out */ hurtThisTurn: number | null; lastDamage: number; attackedBy: Attacker[]; timesAttacked: number; isActive: boolean; activeTurns: number; /** * This is for Fake-Out-likes specifically - it mostly counts how many move * actions you've had since the last time you switched in, so 1/turn normally, * +1 for Dancer/Instruct, -1 for shifting/Sky Drop. * * Incremented before the move is used, so the first move use has * `activeMoveActions === 1`. * * Unfortunately, Truant counts Mega Evolution as an action and Fake * Out doesn't, meaning that Truant can't use this number. */ activeMoveActions: number; previouslySwitchedIn: number; truantTurn: boolean; // Gen 9 only swordBoost: boolean; shieldBoost: boolean; syrupTriggered: boolean; stellarBoostedTypes: string[]; /** Have this pokemon's Start events run yet? (Start events run every switch-in) */ isStarted: boolean; duringMove: boolean; weighthg: number; speed: number; canMegaEvo: string | null | undefined; canMegaEvoX: string | null | undefined; canMegaEvoY: string | null | undefined; canUltraBurst: string | null | undefined; readonly canGigantamax: string | null; /** * A Pokemon's Tera type if it can Terastallize, false if it is temporarily unable to tera and should have its * ability restored upon switching out, or null if its inability to tera is permanent. */ canTerastallize: string | false | null; teraType: string; baseTypes: string[]; terastallized?: string; /** A Pokemon's currently 'staleness' with respect to the Endless Battle Clause. */ staleness?: 'internal' | 'external'; /** Staleness that will be set once a future action occurs (eg. eating a berry). */ pendingStaleness?: 'internal' | 'external'; /** Temporary staleness that lasts only until the Pokemon switches. */ volatileStaleness?: 'external'; // Gen 1 only modifiedStats?: StatsExceptHPTable; modifyStat?: (this: Pokemon, statName: StatIDExceptHP, modifier: number) => void; // Stadium only recalculateStats?: (this: Pokemon) => void; /** * An object for storing untyped data, for mods to use. */ m: { innate?: string, // Partners in Crime originalSpecies?: string, // Mix and Mega [key: string]: any, }; constructor(set: string | AnyObject, side: Side) { this.side = side; this.battle = side.battle; this.m = {}; const pokemonScripts = this.battle.format.pokemon || this.battle.dex.data.Scripts.pokemon; if (pokemonScripts) Object.assign(this, pokemonScripts); if (typeof set === 'string') set = { name: set }; this.baseSpecies = this.battle.dex.species.get(set.species || set.name); if (!this.baseSpecies.exists) { throw new Error(`Unidentified species: ${this.baseSpecies.name}`); } this.set = set as PokemonSet; this.species = this.baseSpecies; if (set.name === set.species || !set.name) { set.name = this.baseSpecies.baseSpecies; } this.speciesState = this.battle.initEffectState({ id: this.species.id }); this.name = set.name.substr(0, 20); this.fullname = `${this.side.id}: ${this.name}`; set.level = this.battle.clampIntRange(set.adjustLevel || set.level || 100, 1, 9999); this.level = set.level; const genders: { [key: string]: GenderName } = { M: 'M', F: 'F', N: 'N' }; this.gender = genders[set.gender] || this.species.gender || (this.battle.random(2) ? 'F' : 'M'); if (this.gender === 'N') this.gender = ''; this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255; this.pokeball = toID(this.set.pokeball) || 'pokeball' as ID; this.dynamaxLevel = typeof set.dynamaxLevel === 'number' ? this.battle.clampIntRange(set.dynamaxLevel, 0, 10) : 10; this.gigantamax = this.set.gigantamax || false; this.baseMoveSlots = []; this.moveSlots = []; if (!this.set.moves?.length) { throw new Error(`Set ${this.name} has no moves`); } for (const moveid of this.set.moves) { let move = this.battle.dex.moves.get(moveid); if (!move.id) continue; if (move.id === 'hiddenpower' && move.type !== 'Normal') { if (!set.hpType) set.hpType = move.type; move = this.battle.dex.moves.get('hiddenpower'); } let basepp = move.noPPBoosts ? move.pp : move.pp * 8 / 5; if (this.battle.gen < 3) basepp = Math.min(61, basepp); this.baseMoveSlots.push({ move: move.name, id: move.id, pp: basepp, maxpp: basepp, target: move.target, disabled: false, disabledSource: '', used: false, }); } this.position = 0; this.details = this.getUpdatedDetails(); this.status = ''; this.statusState = this.battle.initEffectState({}); this.volatiles = {}; this.showCure = undefined; 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 }; } const stats: StatsTable = { hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31 }; let stat: StatID; for (stat in stats) { if (!this.set.evs[stat]) this.set.evs[stat] = 0; if (!this.set.ivs[stat] && this.set.ivs[stat] !== 0) this.set.ivs[stat] = 31; } for (stat in this.set.evs) { this.set.evs[stat] = this.battle.clampIntRange(this.set.evs[stat], 0, 255); } for (stat in this.set.ivs) { this.set.ivs[stat] = this.battle.clampIntRange(this.set.ivs[stat], 0, 31); } if (this.battle.gen && this.battle.gen <= 2) { // We represent DVs using even IVs. Ensure they are in fact even. for (stat in this.set.ivs) { this.set.ivs[stat] &= 30; } } const hpData = this.battle.dex.getHiddenPower(this.set.ivs); this.hpType = set.hpType || hpData.type; this.hpPower = hpData.power; this.baseHpType = this.hpType; this.baseHpPower = this.hpPower; // initialized in this.setSpecies(this.baseSpecies) this.baseStoredStats = null!; this.storedStats = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0 }; this.boosts = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0, accuracy: 0, evasion: 0 }; this.baseAbility = toID(set.ability); this.ability = this.baseAbility; this.abilityState = this.battle.initEffectState({ id: this.ability, target: this }); this.item = toID(set.item); this.itemState = this.battle.initEffectState({ id: this.item, target: this }); this.lastItem = ''; this.usedItemThisTurn = false; this.ateBerry = false; this.trapped = false; this.maybeTrapped = false; this.maybeDisabled = false; this.illusion = null; this.transformed = false; this.fainted = false; this.faintQueued = false; this.subFainted = null; this.regressionForme = false; this.types = this.baseSpecies.types; this.baseTypes = this.types; this.addedType = ''; this.knownType = true; this.apparentType = this.baseSpecies.types.join('/'); // Every Pokemon has a Terastal type this.teraType = this.set.teraType || this.types[0]; this.switchFlag = false; this.forceSwitchFlag = false; this.skipBeforeSwitchOutEventFlag = false; this.draggedIn = null; this.newlySwitched = false; this.beingCalledBack = false; this.lastMove = null; // This is used in gen 2 only, here to avoid code repetition. // Only declared if gen 2 to avoid declaring an object we aren't going to need. if (this.battle.gen === 2) this.lastMoveEncore = null; this.lastMoveUsed = null; this.moveThisTurn = ''; this.statsRaisedThisTurn = false; this.statsLoweredThisTurn = false; this.hurtThisTurn = null; this.lastDamage = 0; this.attackedBy = []; this.timesAttacked = 0; this.isActive = false; this.activeTurns = 0; this.activeMoveActions = 0; this.previouslySwitchedIn = 0; this.truantTurn = false; this.swordBoost = false; this.shieldBoost = false; this.syrupTriggered = false; this.stellarBoostedTypes = []; this.isStarted = false; this.duringMove = false; this.weighthg = 1; this.speed = 0; this.canMegaEvo = this.battle.actions.canMegaEvo(this); this.canMegaEvoX = this.battle.actions.canMegaEvoX?.(this); this.canMegaEvoY = this.battle.actions.canMegaEvoY?.(this); this.canUltraBurst = this.battle.actions.canUltraBurst(this); this.canGigantamax = this.baseSpecies.canGigantamax || null; this.canTerastallize = this.battle.actions.canTerastallize(this); // 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 = 0; this.baseMaxhp = 0; this.hp = 0; this.clearVolatile(); this.hp = this.maxhp; } toJSON(): AnyObject { return State.serializePokemon(this); } get moves(): readonly string[] { return this.moveSlots.map(moveSlot => moveSlot.id); } get baseMoves(): readonly string[] { return this.baseMoveSlots.map(moveSlot => moveSlot.id); } getSlot(): PokemonSlot { const positionOffset = Math.floor(this.side.n / 2) * this.side.active.length; const positionLetter = 'abcdef'.charAt(this.position + positionOffset); return (this.side.id + positionLetter) as PokemonSlot; } toString() { const fullname = (this.illusion) ? this.illusion.fullname : this.fullname; return this.isActive ? this.getSlot() + fullname.slice(2) : fullname; } getUpdatedDetails(level?: number) { let name = this.species.name; if (['Greninja-Bond', 'Rockruff-Dusk'].includes(name)) name = this.species.baseSpecies; if (!level) level = this.level; return name + (level === 100 ? '' : `, L${level}`) + (this.gender === '' ? '' : `, ${this.gender}`) + (this.set.shiny ? ', shiny' : ''); } getFullDetails = () => { const health = this.getHealth(); let details = this.details; if (this.illusion) { details = this.illusion.getUpdatedDetails( this.battle.ruleTable.has('illusionlevelmod') ? this.illusion.level : this.level ); } if (this.terastallized) details += `, tera:${this.terastallized}`; return { side: health.side, secret: `${details}|${health.secret}`, shared: `${details}|${health.shared}` }; }; updateSpeed() { this.speed = this.getActionSpeed(); } calculateStat(statName: StatIDExceptHP, boost: number, modifier?: number, statUser?: Pokemon) { statName = toID(statName) as StatIDExceptHP; // @ts-expect-error type checking prevents 'hp' from being passed, but we're paranoid if (statName === 'hp') throw new Error("Please read `maxhp` directly"); // base stat let stat = this.storedStats[statName]; // Wonder Room swaps defenses before calculating anything else if ('wonderroom' in this.battle.field.pseudoWeather) { if (statName === 'def') { stat = this.storedStats['spd']; } else if (statName === 'spd') { stat = this.storedStats['def']; } } // stat boosts let boosts: SparseBoostsTable = {}; const boostName = statName as BoostID; boosts[boostName] = boost; boosts = this.battle.runEvent('ModifyBoost', statUser || this, null, null, boosts); boost = boosts[boostName]!; const 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 return this.battle.modify(stat, (modifier || 1)); } getStat(statName: StatIDExceptHP, unboosted?: boolean, unmodified?: boolean) { statName = toID(statName) as StatIDExceptHP; // @ts-expect-error type checking prevents 'hp' from being passed, but we're paranoid if (statName === 'hp') throw new Error("Please read `maxhp` directly"); // base stat let stat = this.storedStats[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.field.pseudoWeather) { if (statName === 'def') { statName = 'spd'; } else if (statName === 'spd') { statName = 'def'; } } // stat boosts if (!unboosted) { const boosts = this.battle.runEvent('ModifyBoost', this, null, null, { ...this.boosts }); let boost = boosts[statName]; const 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) { const statTable: { [s in StatIDExceptHP]: string } = { atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe' }; stat = this.battle.runEvent('Modify' + statTable[statName], this, null, null, stat); } if (statName === 'spe' && stat > 10000 && !this.battle.format.battle?.trunc) stat = 10000; return stat; } getActionSpeed() { let speed = this.getStat('spe', false, false); const trickRoomCheck = this.battle.ruleTable.has('twisteddimensionmod') ? !this.battle.field.getPseudoWeather('trickroom') : this.battle.field.getPseudoWeather('trickroom'); if (trickRoomCheck) { speed = 10000 - speed; } return this.battle.trunc(speed, 13); } /** * Gets the Pokemon's best stat. * Moved to its own method due to frequent use of the same code. * Used by Beast Boost, Quark Drive, and Protosynthesis. */ getBestStat(unboosted?: boolean, unmodified?: boolean): StatIDExceptHP { let statName: StatIDExceptHP = 'atk'; let bestStat = 0; const stats: StatIDExceptHP[] = ['atk', 'def', 'spa', 'spd', 'spe']; for (const i of stats) { if (this.getStat(i, unboosted, unmodified) > bestStat) { statName = i; bestStat = this.getStat(i, unboosted, unmodified); } } return statName; } /* Commented out for now until a use for Combat Power is found in Let's Go getCombatPower() { let statSum = 0; let awakeningSum = 0; for (const stat in this.stats) { statSum += this.calculateStat(stat, this.boosts[stat as BoostName]); awakeningSum += this.calculateStat( stat, this.boosts[stat as BoostName]) + this.set.evs[stat]; } const combatPower = Math.floor(Math.floor(statSum * this.level * 6 / 100) + (Math.floor(awakeningSum) * Math.floor((this.level * 4) / 100 + 2))); return this.battle.clampIntRange(combatPower, 0, 10000); } */ getWeight() { const weighthg = this.battle.runEvent('ModifyWeight', this, null, null, this.weighthg); return Math.max(1, weighthg); } getMoveData(move: string | Move) { move = this.battle.dex.moves.get(move); for (const moveSlot of this.moveSlots) { if (moveSlot.id === move.id) { return moveSlot; } } return null; } getMoveHitData(move: ActiveMove) { if (!move.moveHitData) move.moveHitData = {}; const slot = this.getSlot(); return move.moveHitData[slot] || (move.moveHitData[slot] = { crit: false, typeMod: 0, zBrokeProtect: false, }); } alliesAndSelf(): Pokemon[] { return this.side.allies(); } allies(): Pokemon[] { return this.side.allies().filter(ally => ally !== this); } adjacentAllies(): Pokemon[] { return this.side.allies().filter(ally => this.isAdjacent(ally)); } foes(all?: boolean): Pokemon[] { return this.side.foes(all); } adjacentFoes(): Pokemon[] { if (this.battle.activePerHalf <= 2) return this.side.foes(); return this.side.foes().filter(foe => this.isAdjacent(foe)); } isAlly(pokemon: Pokemon | null) { return !!pokemon && (this.side === pokemon.side || this.side.allySide === pokemon.side); } isAdjacent(pokemon2: Pokemon) { if (this.fainted || pokemon2.fainted) return false; if (this.battle.activePerHalf <= 2) return this !== pokemon2; if (this.side === pokemon2.side) return Math.abs(this.position - pokemon2.position) === 1; return Math.abs(this.position + pokemon2.position + 1 - this.side.active.length) <= 1; } getUndynamaxedHP(amount?: number) { const hp = amount || this.hp; if (this.volatiles['dynamax']) { return Math.ceil(hp * this.baseMaxhp / this.maxhp); } return hp; } /** Get targets for Dragon Darts */ getSmartTargets(target: Pokemon, move: ActiveMove) { const target2 = target.adjacentAllies()[0]; if (!target2 || target2 === this || !target2.hp) { move.smartTarget = false; return [target]; } if (!target.hp) { move.smartTarget = false; return [target2]; } return [target, target2]; } getAtLoc(targetLoc: number) { let side = this.battle.sides[targetLoc < 0 ? this.side.n % 2 : (this.side.n + 1) % 2]; targetLoc = Math.abs(targetLoc); if (targetLoc > side.active.length) { targetLoc -= side.active.length; side = this.battle.sides[side.n + 2]; } return side.active[targetLoc - 1]; } /** * Returns a relative location: 1-3, positive for foe, and negative for ally. * Use `getAtLoc` to reverse. */ getLocOf(target: Pokemon) { const positionOffset = Math.floor(target.side.n / 2) * target.side.active.length; const position = target.position + positionOffset + 1; const sameHalf = (this.side.n % 2) === (target.side.n % 2); return sameHalf ? -position : position; } getMoveTargets(move: ActiveMove, target: Pokemon): { targets: Pokemon[], pressureTargets: Pokemon[] } { let targets: Pokemon[] = []; switch (move.target) { case 'all': case 'foeSide': case 'allySide': case 'allyTeam': if (!move.target.startsWith('foe')) { targets.push(...this.alliesAndSelf()); } if (!move.target.startsWith('ally')) { targets.push(...this.foes(true)); } if (targets.length && !targets.includes(target)) { this.battle.retargetLastMove(targets[targets.length - 1]); } break; case 'allAdjacent': targets.push(...this.adjacentAllies()); // falls through case 'allAdjacentFoes': targets.push(...this.adjacentFoes()); if (targets.length && !targets.includes(target)) { this.battle.retargetLastMove(targets[targets.length - 1]); } break; case 'allies': targets = this.alliesAndSelf(); break; default: const selectedTarget = target; if (!target || (target.fainted && !target.isAlly(this)) && this.battle.gameType !== 'freeforall') { // If a targeted foe faints, the move is retargeted const possibleTarget = this.battle.getRandomTarget(this, move); if (!possibleTarget) return { targets: [], pressureTargets: [] }; target = possibleTarget; } if (this.battle.activePerHalf > 1 && !move.tracksTarget) { const isCharging = move.flags['charge'] && !this.volatiles['twoturnmove'] && !(move.id.startsWith('solarb') && ['sunnyday', 'desolateland'].includes(this.effectiveWeather())) && !(move.id === 'electroshot' && ['raindance', 'primordialsea'].includes(this.effectiveWeather())) && !(this.hasItem('powerherb') && move.id !== 'skydrop'); if (!isCharging) { target = this.battle.priorityEvent('RedirectTarget', this, this, move, target); } } if (move.smartTarget) { targets = this.getSmartTargets(target, move); target = targets[0]; } else { targets.push(target); } if (target.fainted && !move.flags['futuremove']) { return { targets: [], pressureTargets: [] }; } if (selectedTarget !== target) { this.battle.retargetLastMove(target); } } // Resolve apparent targets for Pressure. let pressureTargets = targets; if (move.target === 'foeSide') { pressureTargets = []; } if (move.flags['mustpressure']) { pressureTargets = this.foes(); } return { targets, pressureTargets }; } ignoringAbility() { if (this.battle.gen >= 5 && !this.isActive) return true; // Certain Abilities won't activate while Transformed, even if they ordinarily couldn't be suppressed (e.g. Disguise) if (this.getAbility().flags['notransform'] && this.transformed) return true; if (this.getAbility().flags['cantsuppress']) return false; if (this.volatiles['gastroacid']) return true; // Check if any active pokemon have the ability Neutralizing Gas if (this.hasItem('Ability Shield') || this.ability === ('neutralizinggas' as ID)) return false; for (const pokemon of this.battle.getAllActive()) { // can't use hasAbility because it would lead to infinite recursion if (pokemon.ability === ('neutralizinggas' as ID) && !pokemon.volatiles['gastroacid'] && !pokemon.transformed && !pokemon.abilityState.ending && !this.volatiles['commanding']) { return true; } } return false; } ignoringItem() { return !this.getItem().isPrimalOrb && !!( this.itemState.knockedOff || // Gen 3-4 (this.battle.gen >= 5 && !this.isActive) || (!this.getItem().ignoreKlutz && this.hasAbility('klutz')) || this.volatiles['embargo'] || this.battle.field.pseudoWeather['magicroom'] ); } deductPP(move: string | Move, amount?: number | null, target?: Pokemon | null | false) { const gen = this.battle.gen; move = this.battle.dex.moves.get(move); const ppData = this.getMoveData(move); if (!ppData) return 0; ppData.used = true; if (!ppData.pp && gen > 1) return 0; if (!amount) amount = 1; ppData.pp -= amount; if (ppData.pp < 0 && gen > 1) { amount += ppData.pp; ppData.pp = 0; } return amount; } moveUsed(move: ActiveMove, targetLoc?: number) { this.lastMove = move; if (this.battle.gen === 2) this.lastMoveEncore = move; this.lastMoveTargetLoc = targetLoc; this.moveThisTurn = move.id; } gotAttacked(move: string | Move, damage: number | false | undefined, source: Pokemon) { const damageNumber = (typeof damage === 'number') ? damage : 0; move = this.battle.dex.moves.get(move); this.attackedBy.push({ source, damage: damageNumber, move: move.id, thisTurn: true, slot: source.getSlot(), damageValue: damage, }); } getLastAttackedBy() { if (this.attackedBy.length === 0) return undefined; return this.attackedBy[this.attackedBy.length - 1]; } getLastDamagedBy(filterOutSameSide: boolean) { const damagedBy: Attacker[] = this.attackedBy.filter(attacker => ( typeof attacker.damageValue === 'number' && (filterOutSameSide === undefined || !this.isAlly(attacker.source)) )); if (damagedBy.length === 0) return undefined; return damagedBy[damagedBy.length - 1]; } /** * This refers to multi-turn moves like SolarBeam and Outrage and * Sky Drop, which remove all choice (no dynamax, switching, etc). * Don't use it for "soft locks" like Choice Band. */ getLockedMove(): ID | null { const lockedMove = this.battle.runEvent('LockMove', this); return (lockedMove === true) ? null : lockedMove; } getMoves(lockedMove?: ID | null, restrictData?: boolean): { move: string, id: ID, disabled?: string | boolean, disabledSource?: string, target?: string, pp?: number, maxpp?: number, }[] { if (lockedMove) { lockedMove = toID(lockedMove); this.trapped = true; if (lockedMove === 'recharge') { return [{ move: 'Recharge', id: 'recharge' as ID, }]; } for (const moveSlot of this.moveSlots) { if (moveSlot.id !== lockedMove) continue; return [{ move: moveSlot.move, id: moveSlot.id, }]; } // does this happen? return [{ move: this.battle.dex.moves.get(lockedMove).name, id: lockedMove, }]; } const moves = []; let hasValidMove = false; for (const moveSlot of this.moveSlots) { let moveName = moveSlot.move; if (moveSlot.id === 'hiddenpower') { moveName = `Hidden Power ${this.hpType}`; if (this.battle.gen < 6) moveName += ` ${this.hpPower}`; } else if (moveSlot.id === 'return' || moveSlot.id === 'frustration') { const basePowerCallback = this.battle.dex.moves.get(moveSlot.id).basePowerCallback as (pokemon: Pokemon) => number; moveName += ` ${basePowerCallback(this)}`; } let target = moveSlot.target; switch (moveSlot.id) { case 'curse': if (!this.hasType('Ghost')) { target = this.battle.dex.moves.get('curse').nonGhostTarget; } break; case 'pollenpuff': // Heal Block only prevents Pollen Puff from targeting an ally when the user has Heal Block if (this.volatiles['healblock']) { target = 'adjacentFoe'; } break; case 'terastarstorm': if (this.species.name === 'Terapagos-Stellar') { target = 'allAdjacentFoes'; } break; } let disabled = moveSlot.disabled; if (this.volatiles['dynamax']) { // if each of a Pokemon's base moves are disabled by one of these effects, it will Struggle const canCauseStruggle = ['Encore', 'Disable', 'Taunt', 'Assault Vest', 'Belch', 'Stuff Cheeks']; disabled = this.maxMoveDisabled(moveSlot.id) || disabled && canCauseStruggle.includes(moveSlot.disabledSource!); } else if ( (moveSlot.pp <= 0 && !this.volatiles['partialtrappinglock']) || disabled && this.side.active.length >= 2 && this.battle.actions.targetTypeChoices(target!) ) { disabled = true; } if (!disabled) { hasValidMove = true; } else if (disabled === 'hidden' && restrictData) { disabled = false; } moves.push({ move: moveName, id: moveSlot.id, pp: moveSlot.pp, maxpp: moveSlot.maxpp, target, disabled, }); } return hasValidMove ? moves : []; } /** This should be passed the base move and not the corresponding max move so we can check how much PP is left. */ maxMoveDisabled(baseMove: Move | string) { baseMove = this.battle.dex.moves.get(baseMove); if (!this.getMoveData(baseMove.id)?.pp) return true; return !!(baseMove.category === 'Status' && (this.hasItem('assaultvest') || this.volatiles['taunt'])); } getDynamaxRequest(skipChecks?: boolean) { // {gigantamax?: string, maxMoves: {[k: string]: string} | null}[] if (!skipChecks) { if (!this.side.canDynamaxNow()) return; if ( this.species.isMega || this.species.isPrimal || this.species.forme === "Ultra" || this.getItem().zMove || this.canMegaEvo ) { return; } // Some pokemon species are unable to dynamax if (this.species.cannotDynamax || this.illusion?.species.cannotDynamax) return; } const result: DynamaxOptions = { maxMoves: [] }; let atLeastOne = false; for (const moveSlot of this.moveSlots) { const move = this.battle.dex.moves.get(moveSlot.id); const maxMove = this.battle.actions.getMaxMove(move, this); if (maxMove) { if (this.maxMoveDisabled(move)) { result.maxMoves.push({ move: maxMove.id, target: maxMove.target, disabled: true }); } else { result.maxMoves.push({ move: maxMove.id, target: maxMove.target }); atLeastOne = true; } } } if (!atLeastOne) return; if (this.canGigantamax) result.gigantamax = this.canGigantamax; return result; } getMoveRequestData() { let lockedMove = this.getLockedMove(); // Information should be restricted for the last active Pokémon const isLastActive = this.isLastActive(); const canSwitchIn = this.battle.canSwitch(this.side) > 0; let moves = this.getMoves(lockedMove, isLastActive); if (!moves.length) { moves = [{ move: 'Struggle', id: 'struggle' as ID, target: 'randomNormal', disabled: false }]; lockedMove = 'struggle' as ID; } const data: PokemonMoveRequestData = { moves, }; if (isLastActive) { if (this.maybeDisabled) { data.maybeDisabled = true; } if (canSwitchIn) { if (this.trapped === true) { data.trapped = true; } else if (this.maybeTrapped) { data.maybeTrapped = true; } } } else if (canSwitchIn) { // Discovered by selecting a valid Pokémon as a switch target and cancelling. if (this.trapped) data.trapped = true; } if (!lockedMove) { if (this.canMegaEvo) data.canMegaEvo = true; if (this.canMegaEvoX) data.canMegaEvoX = true; if (this.canMegaEvoY) data.canMegaEvoY = true; if (this.canUltraBurst) data.canUltraBurst = true; const canZMove = this.battle.actions.canZMove(this); if (canZMove) data.canZMove = canZMove; if (this.getDynamaxRequest()) data.canDynamax = true; if (data.canDynamax || this.volatiles['dynamax']) data.maxMoves = this.getDynamaxRequest(true); if (this.canTerastallize) data.canTerastallize = this.canTerastallize; } return data; } getSwitchRequestData(forAlly?: boolean): PokemonSwitchRequestData { const entry: PokemonSwitchRequestData = { ident: this.fullname, details: this.details, condition: this.getHealth().secret, active: (this.position < this.side.active.length), stats: { atk: this.baseStoredStats['atk'], def: this.baseStoredStats['def'], spa: this.baseStoredStats['spa'], spd: this.baseStoredStats['spd'], spe: this.baseStoredStats['spe'], }, moves: this[forAlly ? 'baseMoves' : 'moves'].map(move => { if (move === 'hiddenpower') { return `${move}${toID(this.hpType)}${this.battle.gen < 6 ? '' : this.hpPower}` as ID; } if (move === 'frustration' || move === 'return') { const basePowerCallback = this.battle.dex.moves.get(move).basePowerCallback as (pokemon: Pokemon) => number; return `${move}${basePowerCallback(this)}` as ID; } return move as ID; }), baseAbility: this.baseAbility, item: this.item, pokeball: this.pokeball, }; if (this.battle.gen > 6) entry.ability = this.ability; if (this.battle.gen >= 9) { entry.commanding = !!this.volatiles['commanding'] && !this.fainted; entry.reviving = this.isActive && !!this.side.slotConditions[this.position]['revivalblessing']; } if (this.battle.gen === 9) { entry.teraType = this.teraType; entry.terastallized = this.terastallized || ''; } return entry; } isLastActive() { if (!this.isActive) return false; const allyActive = this.side.active; for (let i = this.position + 1; i < allyActive.length; i++) { if (allyActive[i] && !allyActive[i].fainted) return false; } return true; } positiveBoosts() { let boosts = 0; let boost: BoostID; for (boost in this.boosts) { if (this.boosts[boost] > 0) boosts += this.boosts[boost]; } return boosts; } getCappedBoost(boosts: SparseBoostsTable) { const cappedBoost: SparseBoostsTable = {}; let boostName: BoostID; for (boostName in boosts) { const boost = boosts[boostName]; if (!boost) continue; cappedBoost[boostName] = this.battle.clampIntRange(this.boosts[boostName] + boost, -6, 6) - this.boosts[boostName]; } return cappedBoost; } boostBy(boosts: SparseBoostsTable) { boosts = this.getCappedBoost(boosts); let delta = 0; let boostName: BoostID; for (boostName in boosts) { delta = boosts[boostName]!; this.boosts[boostName] += delta; } return delta; } clearBoosts() { let boostName: BoostID; for (boostName in this.boosts) { this.boosts[boostName] = 0; } } setBoost(boosts: SparseBoostsTable) { let boostName: BoostID; for (boostName in boosts) { this.boosts[boostName] = boosts[boostName]!; } } copyVolatileFrom(pokemon: Pokemon, switchCause?: string | boolean) { this.clearVolatile(); if (switchCause !== 'shedtail') this.boosts = pokemon.boosts; for (const i in pokemon.volatiles) { if (switchCause === 'shedtail' && i !== 'substitute') continue; if (this.battle.dex.conditions.getByID(i as ID).noCopy) continue; // shallow clones this.volatiles[i] = this.battle.initEffectState({ ...pokemon.volatiles[i] }); if (this.volatiles[i].linkedPokemon) { delete pokemon.volatiles[i].linkedPokemon; delete pokemon.volatiles[i].linkedStatus; for (const linkedPoke of this.volatiles[i].linkedPokemon) { const linkedPokeLinks = linkedPoke.volatiles[this.volatiles[i].linkedStatus].linkedPokemon; linkedPokeLinks[linkedPokeLinks.indexOf(pokemon)] = this; } } } pokemon.clearVolatile(); for (const i in this.volatiles) { const volatile = this.getVolatile(i) as Condition; this.battle.singleEvent('Copy', volatile, this.volatiles[i], this); } } transformInto(pokemon: Pokemon, effect?: Effect) { const species = pokemon.species; if ( pokemon.fainted || this.illusion || pokemon.illusion || (pokemon.volatiles['substitute'] && this.battle.gen >= 5) || (pokemon.transformed && this.battle.gen >= 2) || (this.transformed && this.battle.gen >= 5) || species.name === 'Eternatus-Eternamax' || (['Ogerpon', 'Terapagos'].includes(species.baseSpecies) && (this.terastallized || pokemon.terastallized)) || this.terastallized === 'Stellar' ) { return false; } if (this.battle.dex.currentMod === 'gen1stadium' && ( species.name === 'Ditto' || (this.species.name === 'Ditto' && pokemon.moves.includes('transform')) )) { return false; } if (!this.setSpecies(species, effect, true)) return false; this.transformed = true; this.weighthg = pokemon.weighthg; const types = pokemon.getTypes(true, true); this.setType(pokemon.volatiles['roost'] ? pokemon.volatiles['roost'].typeWas : types, true); this.addedType = pokemon.addedType; this.knownType = this.isAlly(pokemon) && pokemon.knownType; this.apparentType = pokemon.apparentType; let statName: StatIDExceptHP; for (statName in this.storedStats) { this.storedStats[statName] = pokemon.storedStats[statName]; if (this.modifiedStats) this.modifiedStats[statName] = pokemon.modifiedStats![statName]; // Gen 1: Copy modified stats. } this.moveSlots = []; this.hpType = (this.battle.gen >= 5 ? this.hpType : pokemon.hpType); this.hpPower = (this.battle.gen >= 5 ? this.hpPower : pokemon.hpPower); this.timesAttacked = pokemon.timesAttacked; for (const moveSlot of pokemon.moveSlots) { let moveName = moveSlot.move; if (moveSlot.id === 'hiddenpower') { moveName = 'Hidden Power ' + this.hpType; } this.moveSlots.push({ move: moveName, id: moveSlot.id, pp: moveSlot.maxpp === 1 ? 1 : 5, maxpp: this.battle.gen >= 5 ? (moveSlot.maxpp === 1 ? 1 : 5) : moveSlot.maxpp, target: moveSlot.target, disabled: false, used: false, virtual: true, }); } let boostName: BoostID; for (boostName in pokemon.boosts) { this.boosts[boostName] = pokemon.boosts[boostName]; } if (this.battle.gen >= 6) { // we need to remove all of the overlapping crit volatiles before adding any of them const volatilesToCopy = ['dragoncheer', 'focusenergy', 'gmaxchistrike', 'laserfocus']; for (const volatile of volatilesToCopy) this.removeVolatile(volatile); for (const volatile of volatilesToCopy) { if (pokemon.volatiles[volatile]) { this.addVolatile(volatile); if (volatile === 'gmaxchistrike') this.volatiles[volatile].layers = pokemon.volatiles[volatile].layers; if (volatile === 'dragoncheer') this.volatiles[volatile].hasDragonType = pokemon.volatiles[volatile].hasDragonType; } } } if (effect) { this.battle.add('-transform', this, pokemon, '[from] ' + effect.fullname); } else { this.battle.add('-transform', this, pokemon); } if (this.terastallized) { this.knownType = true; this.apparentType = this.terastallized; } if (this.battle.gen > 2) this.setAbility(pokemon.ability, this, true, true); // 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.species.num === 487) { // Giratina formes if (this.species.name === 'Giratina' && this.item === 'griseousorb') { this.formeChange('Giratina-Origin'); } else if (this.species.name === 'Giratina-Origin' && this.item !== 'griseousorb') { this.formeChange('Giratina'); } } if (this.species.num === 493) { // Arceus formes const item = this.getItem(); const targetForme = (item?.onPlate ? 'Arceus-' + item.onPlate : 'Arceus'); if (this.species.name !== targetForme) { this.formeChange(targetForme); } } } // Pokemon transformed into Ogerpon cannot Terastallize // restoring their ability to tera after they untransform is handled ELSEWHERE if (['Ogerpon', 'Terapagos'].includes(this.species.baseSpecies) && this.canTerastallize) this.canTerastallize = false; return true; } /** * Changes this Pokemon's species to the given speciesId (or species). * This function only handles changes to stats and type. * Use formeChange to handle changes to ability and sending client messages. */ setSpecies(rawSpecies: Species, source: Effect | null = this.battle.effect, isTransform = false) { const species = this.battle.runEvent('ModifySpecies', this, null, source, rawSpecies); if (!species) return null; this.species = species; this.setType(species.types, true); this.apparentType = rawSpecies.types.join('/'); this.addedType = species.addedType || ''; this.knownType = true; this.weighthg = species.weighthg; const stats = this.battle.spreadModify(this.species.baseStats, this.set); if (this.species.maxHP) stats.hp = this.species.maxHP; if (!this.maxhp) { this.baseMaxhp = stats.hp; this.maxhp = stats.hp; this.hp = stats.hp; } if (!isTransform) this.baseStoredStats = stats; let statName: StatIDExceptHP; for (statName in this.storedStats) { this.storedStats[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. if (this.status === 'par') this.modifyStat!('spe', 0.25); if (this.status === 'brn') this.modifyStat!('atk', 0.5); } this.speed = this.storedStats.spe; return species; } /** * Changes this Pokemon's forme to match the given speciesId (or species). * This function handles all changes to stats, ability, type, species, etc. * as well as sending all relevant messages sent to the client. */ formeChange( speciesId: string | Species, source: Effect | null = this.battle.effect, isPermanent?: boolean, abilitySlot = '0', message?: string ) { const rawSpecies = this.battle.dex.species.get(speciesId); const species = this.setSpecies(rawSpecies, source); if (!species) return false; if (this.battle.gen <= 2) return true; // The species the opponent sees const apparentSpecies = this.illusion ? this.illusion.species.name : species.baseSpecies; if (isPermanent) { if (!this.transformed) this.regressionForme = true; this.baseSpecies = rawSpecies; this.details = this.getUpdatedDetails(); let details = (this.illusion || this).details; if (this.terastallized) details += `, tera:${this.terastallized}`; this.battle.add('detailschange', this, details); if (!source) { // Tera forme // Ogerpon/Terapagos text goes here } else if (source.effectType === 'Item') { this.canTerastallize = null; // National Dex behavior if (source.zMove) { this.battle.add('-burst', this, apparentSpecies, species.requiredItem); this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant } else if (source.isPrimalOrb) { if (this.illusion) { this.ability = ''; this.battle.add('-primal', this.illusion, species.requiredItem); } else { this.battle.add('-primal', this, species.requiredItem); } } else { this.battle.add('-mega', this, apparentSpecies, species.requiredItem); this.moveThisTurnResult = true; // Mega Evolution counts as an action for Truant } } else if (source.effectType === 'Status') { // Shaymin-Sky -> Shaymin this.battle.add('-formechange', this, species.name, message); } } else { if (source?.effectType === 'Ability') { this.battle.add('-formechange', this, species.name, message, `[from] ability: ${source.name}`); } else { this.battle.add('-formechange', this, this.illusion ? this.illusion.species.name : species.name, message); } } if (isPermanent && (!source || !['disguise', 'iceface'].includes(source.id))) { if (this.illusion) { this.ability = ''; // Don't allow Illusion to wear off } const ability = species.abilities[abilitySlot] || species.abilities['0']; // Ogerpon's forme change doesn't override permanent abilities if (source || !this.getAbility().flags['cantsuppress']) this.setAbility(ability, null, true); // However, its ability does reset upon switching out this.baseAbility = toID(ability); } if (this.terastallized) { this.knownType = true; this.apparentType = this.terastallized; } return true; } clearVolatile(includeSwitchFlags = true) { 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' as ID) && !this.transformed) { const moveslot = this.baseMoves.indexOf('mimic' as ID); const mimicPP = this.moveSlots[moveslot] ? this.moveSlots[moveslot].pp : 16; this.moveSlots = this.baseMoveSlots.slice(); this.moveSlots[moveslot].pp = mimicPP; } else { this.moveSlots = this.baseMoveSlots.slice(); } this.transformed = false; this.ability = this.baseAbility; this.hpType = this.baseHpType; this.hpPower = this.baseHpPower; if (this.canTerastallize === false) this.canTerastallize = this.teraType; for (const i in this.volatiles) { if (this.volatiles[i].linkedStatus) { this.removeLinkedVolatiles(this.volatiles[i].linkedStatus, this.volatiles[i].linkedPokemon); } } if (this.species.name === 'Eternatus-Eternamax' && this.volatiles['dynamax']) { this.volatiles = { dynamax: this.volatiles['dynamax'] }; } else { this.volatiles = {}; } if (includeSwitchFlags) { this.switchFlag = false; this.forceSwitchFlag = false; } this.lastMove = null; if (this.battle.gen === 2) this.lastMoveEncore = null; this.lastMoveUsed = null; this.moveThisTurn = ''; this.moveLastTurnResult = undefined; this.moveThisTurnResult = undefined; this.lastDamage = 0; this.attackedBy = []; this.hurtThisTurn = null; this.newlySwitched = true; this.beingCalledBack = false; this.volatileStaleness = undefined; delete this.abilityState.started; delete this.itemState.started; this.setSpecies(this.baseSpecies); } hasType(type: string | string[]) { const thisTypes = this.getTypes(); if (typeof type === 'string') { return thisTypes.includes(type); } for (const typeName of type) { if (thisTypes.includes(typeName)) return true; } return false; } /** * This function only puts the pokemon in the faint queue; * actually setting of this.fainted comes later when the * faint queue is resolved. * * Returns the amount of damage actually dealt */ faint(source: Pokemon | null = null, effect: Effect | null = null) { if (this.fainted || this.faintQueued) return 0; const d = this.hp; this.hp = 0; this.switchFlag = false; this.faintQueued = true; this.battle.faintQueue.push({ target: this, source, effect, }); return d; } damage(d: number, source: Pokemon | null = null, effect: Effect | null = null) { if (!this.hp || isNaN(d) || d <= 0) return 0; if (d < 1 && d > 0) d = 1; d = this.battle.trunc(d); this.hp -= d; if (this.hp <= 0) { d += this.hp; this.faint(source, effect); } return d; } tryTrap(isHidden = false) { if (!this.runStatusImmunity('trapped')) return false; if (this.trapped && isHidden) return true; this.trapped = isHidden ? 'hidden' : true; return true; } hasMove(moveid: string) { moveid = toID(moveid); if (moveid.substr(0, 11) === 'hiddenpower') moveid = 'hiddenpower'; for (const moveSlot of this.moveSlots) { if (moveid === moveSlot.id) { return moveid; } } return false; } disableMove(moveid: string, isHidden?: boolean | string, sourceEffect?: Effect) { if (!sourceEffect && this.battle.event) { sourceEffect = this.battle.effect; } moveid = toID(moveid); for (const moveSlot of this.moveSlots) { if (moveSlot.id === moveid && moveSlot.disabled !== true) { moveSlot.disabled = (isHidden || true); moveSlot.disabledSource = (sourceEffect?.name || moveSlot.move); } } } /** Returns the amount of damage actually healed */ heal(d: number, source: Pokemon | null = null, effect: Effect | null = null) { if (!this.hp) return false; d = this.battle.trunc(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: number) { if (!this.hp) return 0; d = this.battle.trunc(d); if (isNaN(d)) return; if (d < 1) d = 1; d -= this.hp; this.hp += d; if (this.hp > this.maxhp) { d -= this.hp - this.maxhp; this.hp = this.maxhp; } return d; } trySetStatus(status: string | Condition, source: Pokemon | null = null, sourceEffect: Effect | null = null) { return this.setStatus(this.status || status, source, sourceEffect); } /** Unlike clearStatus, gives cure message */ cureStatus(silent = false) { if (!this.hp || !this.status) return false; this.battle.add('-curestatus', this, this.status, silent ? '[silent]' : '[msg]'); if (this.status === 'slp' && this.removeVolatile('nightmare')) { this.battle.add('-end', this, 'Nightmare', '[silent]'); } this.setStatus(''); return true; } setStatus( status: string | Condition, source: Pokemon | null = null, sourceEffect: Effect | null = null, ignoreImmunities = false ) { if (!this.hp) return false; status = this.battle.dex.conditions.get(status); if (this.battle.event) { if (!source) source = this.battle.event.source; if (!sourceEffect) sourceEffect = this.battle.effect; } if (!source) source = this; if (this.status === status.id) { if ((sourceEffect as Move)?.status === this.status) { this.battle.add('-fail', this, this.status); } else if ((sourceEffect as Move)?.status) { this.battle.add('-fail', source); this.battle.attrLastMove('[still]'); } return false; } if ( !ignoreImmunities && status.id && !(source?.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id)) ) { // the game currently never ignores immunities if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) { this.battle.debug('immune to status'); if ((sourceEffect as Move)?.status) { this.battle.add('-immune', this); } return false; } } const prevStatus = this.status; const prevStatusState = this.statusState; if (status.id) { const result: boolean = 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.statusState = this.battle.initEffectState({ id: status.id, target: this }); if (source) this.statusState.source = source; if (status.duration) this.statusState.duration = status.duration; if (status.durationCallback) { this.statusState.duration = status.durationCallback.call(this.battle, this, source, sourceEffect); } if (status.id && !this.battle.singleEvent('Start', status, this.statusState, this, source, sourceEffect)) { this.battle.debug('status start [' + status.id + '] interrupted'); // cancel the setstatus this.status = prevStatus; this.statusState = prevStatusState; return false; } if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) { return false; } return true; } /** * Unlike cureStatus, does not give cure message */ clearStatus() { if (!this.hp || !this.status) return false; if (this.status === 'slp' && this.removeVolatile('nightmare')) { this.battle.add('-end', this, 'Nightmare', '[silent]'); } this.setStatus(''); return true; } getStatus() { return this.battle.dex.conditions.getByID(this.status); } eatItem(force?: boolean, source?: Pokemon, sourceEffect?: Effect) { if (!this.item || this.itemState.knockedOff) return false; if ((!this.hp && this.item !== 'jabocaberry' && this.item !== 'rowapberry') || !this.isActive) return false; if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect; if (!source && this.battle.event?.target) source = this.battle.event.target; const item = this.getItem(); if (sourceEffect?.effectType === 'Item' && this.item !== sourceEffect.id && source === this) { // if an item is telling us to eat it but we aren't holding it, we probably shouldn't eat what we are holding return false; } if ( this.battle.runEvent('UseItem', this, null, null, item) && (force || this.battle.runEvent('TryEatItem', this, null, null, item)) ) { this.battle.add('-enditem', this, item, '[eat]'); this.battle.singleEvent('Eat', item, this.itemState, this, source, sourceEffect); this.battle.runEvent('EatItem', this, null, null, item); if (RESTORATIVE_BERRIES.has(item.id)) { switch (this.pendingStaleness) { case 'internal': if (this.staleness !== 'external') this.staleness = 'internal'; break; case 'external': this.staleness = 'external'; break; } this.pendingStaleness = undefined; } this.lastItem = this.item; this.item = ''; this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.ateBerry = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; } return false; } useItem(source?: Pokemon, sourceEffect?: Effect) { if ((!this.hp && !this.getItem().isGem) || !this.isActive) return false; if (!this.item || this.itemState.knockedOff) return false; if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect; if (!source && this.battle.event?.target) source = this.battle.event.target; const item = this.getItem(); if (sourceEffect?.effectType === 'Item' && this.item !== sourceEffect.id && source === this) { // if an item is telling us to eat it but we aren't holding it, we probably shouldn't eat what we are holding return false; } 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, '[from] gem'); } else { this.battle.add('-enditem', this, item); } break; } if (item.boosts) { this.battle.boost(item.boosts, this, source, item); } this.battle.singleEvent('Use', item, this.itemState, this, source, sourceEffect); this.lastItem = this.item; this.item = ''; this.battle.clearEffectState(this.itemState); this.usedItemThisTurn = true; this.battle.runEvent('AfterUseItem', this, null, null, item); return true; } return false; } takeItem(source?: Pokemon) { if (!this.isActive) return false; if (!this.item || this.itemState.knockedOff) return false; if (!source) source = this; if (this.battle.gen === 4) { if (toID(this.ability) === 'multitype') return false; if (toID(source.ability) === 'multitype') return false; } const item = this.getItem(); if (this.battle.runEvent('TakeItem', this, source, null, item)) { this.item = ''; const oldItemState = this.itemState; this.battle.clearEffectState(this.itemState); this.pendingStaleness = undefined; this.battle.singleEvent('End', item, oldItemState, this); this.battle.runEvent('AfterTakeItem', this, null, null, item); return item; } return false; } setItem(item: string | Item, source?: Pokemon, effect?: Effect) { if (!this.hp || !this.isActive) return false; if (this.itemState.knockedOff) return false; if (typeof item === 'string') item = this.battle.dex.items.get(item); const effectid = this.battle.effect ? this.battle.effect.id : ''; if (RESTORATIVE_BERRIES.has('leppaberry' as ID)) { const inflicted = ['trick', 'switcheroo'].includes(effectid); const external = inflicted && source && !source.isAlly(this); this.pendingStaleness = external ? 'external' : 'internal'; } else { this.pendingStaleness = undefined; } const oldItem = this.getItem(); const oldItemState = this.itemState; this.item = item.id; this.itemState = this.battle.initEffectState({ id: item.id, target: this }); if (oldItem.exists) this.battle.singleEvent('End', oldItem, oldItemState, this); if (item.id) { this.battle.singleEvent('Start', item, this.itemState, this, source, effect); } return true; } getItem() { return this.battle.dex.items.getByID(this.item); } hasItem(item: string | string[]) { if (Array.isArray(item)) { if (!item.map(toID).includes(this.item)) return false; } else { if (toID(item) !== this.item) return false; } return !this.ignoringItem(); } clearItem() { return this.setItem(''); } setAbility(ability: string | Ability, source?: Pokemon | null, isFromFormeChange = false, isTransform = false) { if (!this.hp) return false; if (typeof ability === 'string') ability = this.battle.dex.abilities.get(ability); const oldAbility = this.ability; if (!isFromFormeChange) { if (ability.flags['cantsuppress'] || this.getAbility().flags['cantsuppress']) return false; } if (!isFromFormeChange && !isTransform) { const setAbilityEvent: boolean | null = this.battle.runEvent('SetAbility', this, source, this.battle.effect, ability); if (!setAbilityEvent) return setAbilityEvent; } this.battle.singleEvent('End', this.battle.dex.abilities.get(oldAbility), this.abilityState, this, source); if (this.battle.effect && this.battle.effect.effectType === 'Move' && !isFromFormeChange) { this.battle.add( '-endability', this, this.battle.dex.abilities.get(oldAbility), `[from] move: ${this.battle.dex.moves.get(this.battle.effect.id)}` ); } this.ability = ability.id; this.abilityState = this.battle.initEffectState({ id: ability.id, target: this }); if (ability.id && this.battle.gen > 3 && (!isTransform || oldAbility !== ability.id || this.battle.gen <= 4)) { this.battle.singleEvent('Start', ability, this.abilityState, this, source); } return oldAbility; } getAbility() { return this.battle.dex.abilities.getByID(this.ability); } hasAbility(ability: string | string[]) { if (Array.isArray(ability)) { if (!ability.map(toID).includes(this.ability)) return false; } else { if (toID(ability) !== this.ability) return false; } return !this.ignoringAbility(); } clearAbility() { return this.setAbility(''); } getNature() { return this.battle.dex.natures.get(this.set.nature); } addVolatile( status: string | Condition, source: Pokemon | null = null, sourceEffect: Effect | null = null, linkedStatus: string | Condition | null = null ): boolean | any { let result; status = this.battle.dex.conditions.get(status); if (!this.hp && !status.affectsFainted) return false; if (linkedStatus && source && !source.hp) return false; if (this.battle.event) { if (!source) source = this.battle.event.source; if (!sourceEffect) sourceEffect = this.battle.effect; } if (!source) source = this; 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)) { this.battle.debug('immune to volatile status'); if ((sourceEffect as Move)?.status) { this.battle.add('-immune', this); } 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] = this.battle.initEffectState({ id: status.id, name: status.name, target: this }); if (source) { this.volatiles[status.id].source = source; this.volatiles[status.id].sourceSlot = source.getSlot(); } 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) { if (!source.volatiles[linkedStatus.toString()]) { source.addVolatile(linkedStatus, this, sourceEffect); source.volatiles[linkedStatus.toString()].linkedPokemon = [this]; source.volatiles[linkedStatus.toString()].linkedStatus = status; } else { source.volatiles[linkedStatus.toString()].linkedPokemon.push(this); } this.volatiles[status.toString()].linkedPokemon = [source]; this.volatiles[status.toString()].linkedStatus = linkedStatus; } return true; } getVolatile(status: string | Effect) { status = this.battle.dex.conditions.get(status) as Effect; if (!this.volatiles[status.id]) return null; return status; } removeVolatile(status: string | Effect) { if (!this.hp) return false; status = this.battle.dex.conditions.get(status) as Effect; if (!this.volatiles[status.id]) return false; const linkedPokemon = this.volatiles[status.id].linkedPokemon; const linkedStatus = this.volatiles[status.id].linkedStatus; this.battle.singleEvent('End', status, this.volatiles[status.id], this); delete this.volatiles[status.id]; if (linkedPokemon) { this.removeLinkedVolatiles(linkedStatus, linkedPokemon); } return true; } removeLinkedVolatiles(linkedStatus: string | Effect, linkedPokemon: Pokemon[]) { linkedStatus = linkedStatus.toString(); for (const linkedPoke of linkedPokemon) { const volatileData = linkedPoke.volatiles[linkedStatus]; if (!volatileData) continue; volatileData.linkedPokemon.splice(volatileData.linkedPokemon.indexOf(this), 1); if (volatileData.linkedPokemon.length === 0) { linkedPoke.removeVolatile(linkedStatus); } } } getHealth = () => { if (!this.hp) return { side: this.side.id, secret: '0 fnt', shared: '0 fnt' }; let secret = `${this.hp}/${this.maxhp}`; let shared; const ratio = this.hp / this.maxhp; if (this.battle.reportExactHP) { shared = secret; } else if (this.battle.reportPercentages || this.battle.gen >= 8) { // HP Percentage Mod mechanics let percentage = Math.ceil(ratio * 100); if ((percentage === 100) && (ratio < 1.0)) { percentage = 99; } shared = `${percentage}/100`; } else { // In-game accurate pixel health mechanics const pixels = Math.floor(ratio * 48) || 1; shared = `${pixels}/48`; if ((pixels === 9) && (ratio > 0.2)) { shared += 'y'; // force yellow HP bar } else if ((pixels === 24) && (ratio > 0.5)) { shared += 'g'; // force green HP bar } } if (this.status) { secret += ` ${this.status}`; shared += ` ${this.status}`; } return { side: this.side.id, secret, shared }; }; /** * Sets a type (except on Arceus, who resists type changes) */ setType(newType: string | string[], enforce = false) { if (!enforce) { // No Pokemon should be able to have Stellar as a base type if (typeof newType === 'string' ? newType === 'Stellar' : newType.includes('Stellar')) return false; // First type of Arceus, Silvally cannot be normally changed if ((this.battle.gen >= 5 && (this.species.num === 493 || this.species.num === 773)) || (this.battle.gen === 4 && this.hasAbility('multitype'))) { return false; } // Terastallized Pokemon cannot have their base type changed except via forme change if (this.terastallized) return false; } if (!newType) throw new Error("Must pass type to setType"); this.types = (typeof newType === 'string' ? [newType] : newType); this.addedType = ''; this.knownType = true; this.apparentType = this.types.join('/'); return true; } /** Removes any types added previously and adds another one. */ addType(newType: string) { if (this.terastallized) return false; this.addedType = newType; return true; } getTypes(excludeAdded?: boolean, preterastallized?: boolean): string[] { if (!preterastallized && this.terastallized && this.terastallized !== 'Stellar') { return [this.terastallized]; } const types = this.battle.runEvent('Type', this, null, null, this.types); if (!types.length) types.push(this.battle.gen >= 5 ? 'Normal' : '???'); if (!excludeAdded && this.addedType) return types.concat(this.addedType); return types; } isGrounded(negateImmunity = false) { if ('gravity' in this.battle.field.pseudoWeather) return true; if ('ingrain' in this.volatiles && this.battle.gen >= 4) return true; if ('smackdown' in this.volatiles) return true; const 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') && !(this.hasType('???') && 'roost' in this.volatiles)) return false; if (this.hasAbility('levitate') && !this.battle.suppressingAbility(this)) return null; if ('magnetrise' in this.volatiles) return false; if ('telekinesis' in this.volatiles) return false; return item !== 'airballoon'; } isSemiInvulnerable() { return (this.volatiles['fly'] || this.volatiles['bounce'] || this.volatiles['dive'] || this.volatiles['dig'] || this.volatiles['phantomforce'] || this.volatiles['shadowforce'] || this.isSkyDropped()); } isSkyDropped() { if (this.volatiles['skydrop']) return true; for (const foeActive of this.side.foe.active) { if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === this) { return true; } } return false; } /** Specifically: is protected against a single-target damaging move */ isProtected() { return !!( this.volatiles['protect'] || this.volatiles['detect'] || this.volatiles['maxguard'] || this.volatiles['kingsshield'] || this.volatiles['spikyshield'] || this.volatiles['banefulbunker'] || this.volatiles['obstruct'] || this.volatiles['silktrap'] || this.volatiles['burningbulwark'] ); } /** * Like Field.effectiveWeather(), but ignores sun and rain if * the Utility Umbrella is active for the Pokemon. */ effectiveWeather() { const weather = this.battle.field.effectiveWeather(); switch (weather) { case 'sunnyday': case 'raindance': case 'desolateland': case 'primordialsea': if (this.hasItem('utilityumbrella')) return ''; } return weather; } runEffectiveness(move: ActiveMove) { let totalTypeMod = 0; if (this.terastallized && move.type === 'Stellar') { totalTypeMod = 1; } else { for (const type of this.getTypes()) { let typeMod = this.battle.dex.getEffectiveness(move, type); typeMod = this.battle.singleEvent('Effectiveness', move, null, this, type, move, typeMod); totalTypeMod += this.battle.runEvent('Effectiveness', this, type, move, typeMod); } } if (this.species.name === 'Terapagos-Terastal' && this.hasAbility('Tera Shell') && !this.battle.suppressingAbility(this)) { if (this.abilityState.resisted) return -1; // all hits of multi-hit move should be not very effective if (move.category === 'Status' || move.id === 'struggle' || !this.runImmunity(move.type) || totalTypeMod < 0 || this.hp < this.maxhp) { return totalTypeMod; } this.battle.add('-activate', this, 'ability: Tera Shell'); this.abilityState.resisted = true; return -1; } return totalTypeMod; } /** false = immune, true = not immune */ runImmunity(type: string, message?: string | boolean) { if (!type || type === '???') return true; if (!this.battle.dex.types.isName(type)) { throw new Error("Use runStatusImmunity for " + type); } if (this.fainted) return false; const negateImmunity = !this.battle.runEvent('NegateImmunity', this, type); const notImmune = type === 'Ground' ? this.isGrounded(negateImmunity) : negateImmunity || this.battle.dex.getImmunity(type, this); if (notImmune) return true; if (!message) return false; if (notImmune === null) { this.battle.add('-immune', this, '[from] ability: Levitate'); } else { this.battle.add('-immune', this); } return false; } runStatusImmunity(type: string, message?: string) { if (this.fainted) return false; if (!type) return true; if (!this.battle.dex.getImmunity(type, this)) { this.battle.debug('natural status immunity'); if (message) { this.battle.add('-immune', this); } return false; } const 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); } return false; } return true; } destroy() { // deallocate ourself // get rid of some possibly-circular references (this as any).battle = null!; (this as any).side = null!; } }