Spaces:
Running
Running
/** | |
* 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!; | |
} | |
} | |