mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-18 19:28:35 -05:00
* Split up dex-data over individual files This commit introduces: - `dex-abilities.ts` - `dex-conditions.ts` - `dex-formats.ts` - `dex-items.ts` - `dex-moves.ts` - `dex-species.ts` These files centralize definitions from `dex-data` and `global-types`. * Inherit ItemData from Item etc Previously, Condition inherited from ConditionData. Now, ConditionData inherits from Condition. The advantage of the new approach is that now, Condition and DataCondition no longer need to be separate types, and there should be much less duplication of type definitions in general. This has also been done for - ItemData/Item/DataItem - AbilityData/Ability/DataAbility - FormatData/Format/DataFormat Species and DataSpecies was already merged, but this also reverses their inheritance (saving a lot of duplicated definitions in the process!) The only one left is MoveData, which is just super complicated and will need its own commit.
1953 lines
60 KiB
TypeScript
1953 lines
60 KiB
TypeScript
/**
|
|
* Simulator Pokemon
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* @license MIT license
|
|
*/
|
|
|
|
import {State} from './state';
|
|
import {toID} from './dex';
|
|
|
|
/** 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;
|
|
}
|
|
|
|
export interface EffectState {
|
|
// TODO: set this to be an actual number after converting data/ to .ts
|
|
duration?: number | any;
|
|
[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;
|
|
readonly fullname: string;
|
|
readonly level: number;
|
|
readonly gender: GenderName;
|
|
readonly happiness: number;
|
|
readonly pokeball: string;
|
|
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;
|
|
|
|
position: number;
|
|
details: string;
|
|
|
|
baseSpecies: Species;
|
|
species: Species;
|
|
speciesData: EffectState;
|
|
|
|
status: ID;
|
|
statusData: 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;
|
|
abilityData: EffectState;
|
|
|
|
item: ID;
|
|
itemData: 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;
|
|
|
|
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;
|
|
switchCopyFlag: boolean;
|
|
draggedIn: number | null;
|
|
newlySwitched: boolean;
|
|
beingCalledBack: boolean;
|
|
|
|
lastMove: 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;
|
|
/** used for Assurance check */
|
|
hurtThisTurn: boolean;
|
|
lastDamage: number;
|
|
attackedBy: {source: Pokemon, damage: number, thisTurn: boolean, move?: ID}[];
|
|
|
|
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;
|
|
/** Have this pokemon's Start events run yet? (Start events run every switch-in) */
|
|
isStarted: boolean;
|
|
duringMove: boolean;
|
|
|
|
weighthg: number;
|
|
speed: number;
|
|
abilityOrder: number;
|
|
|
|
canMegaEvo: string | null | undefined;
|
|
canUltraBurst: string | null | undefined;
|
|
canDynamax: boolean;
|
|
readonly canGigantamax: string | null;
|
|
|
|
/** 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: StatNameExceptHP, modifier: number) => void;
|
|
// Stadium only
|
|
recalculateStats?: (this: Pokemon) => void;
|
|
|
|
/**
|
|
* An object for storing untyped data, for mods to use.
|
|
*/
|
|
m: {
|
|
gluttonyFlag?: boolean, // Gen-NEXT
|
|
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.getSpecies(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.speciesData = {id: this.species.id};
|
|
|
|
this.name = set.name.substr(0, 20);
|
|
this.fullname = this.side.id + ': ' + this.name;
|
|
|
|
set.level = this.battle.clampIntRange(set.forcedLevel || 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 < 1 ? 'M' : 'F');
|
|
if (this.gender === 'N') this.gender = '';
|
|
this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255;
|
|
this.pokeball = this.set.pokeball || 'pokeball';
|
|
this.gigantamax = this.set.gigantamax || false;
|
|
|
|
this.baseMoveSlots = [];
|
|
this.moveSlots = [];
|
|
if (!this.set.moves || !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.getMove(moveid);
|
|
if (!move.id) continue;
|
|
if (move.id === 'hiddenpower' && move.type !== 'Normal') {
|
|
if (!set.hpType) set.hpType = move.type;
|
|
move = this.battle.dex.getMove('hiddenpower');
|
|
}
|
|
this.baseMoveSlots.push({
|
|
move: move.name,
|
|
id: move.id,
|
|
pp: ((move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5),
|
|
maxpp: ((move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5),
|
|
target: move.target,
|
|
disabled: false,
|
|
disabledSource: '',
|
|
used: false,
|
|
});
|
|
}
|
|
|
|
this.position = 0;
|
|
this.details = this.species.name + (this.level === 100 ? '' : ', L' + this.level) +
|
|
(this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : '');
|
|
|
|
this.status = '';
|
|
this.statusData = {};
|
|
this.volatiles = {};
|
|
this.showCure = false;
|
|
|
|
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: StatName;
|
|
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.abilityData = {id: this.ability};
|
|
|
|
this.item = toID(set.item);
|
|
this.itemData = {id: this.item};
|
|
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.types = this.baseSpecies.types;
|
|
this.addedType = '';
|
|
this.knownType = true;
|
|
this.apparentType = this.baseSpecies.types.join('/');
|
|
|
|
this.switchFlag = false;
|
|
this.forceSwitchFlag = false;
|
|
this.switchCopyFlag = false;
|
|
this.draggedIn = null;
|
|
this.newlySwitched = false;
|
|
this.beingCalledBack = false;
|
|
|
|
this.lastMove = null;
|
|
this.moveThisTurn = '';
|
|
this.statsRaisedThisTurn = false;
|
|
this.statsLoweredThisTurn = false;
|
|
this.hurtThisTurn = false;
|
|
this.lastDamage = 0;
|
|
this.attackedBy = [];
|
|
|
|
this.isActive = false;
|
|
this.activeTurns = 0;
|
|
this.activeMoveActions = 0;
|
|
this.previouslySwitchedIn = 0;
|
|
this.truantTurn = false;
|
|
this.isStarted = false;
|
|
this.duringMove = false;
|
|
|
|
this.weighthg = 1;
|
|
this.speed = 0;
|
|
this.abilityOrder = 0;
|
|
|
|
this.canMegaEvo = this.battle.canMegaEvo(this);
|
|
this.canUltraBurst = this.battle.canUltraBurst(this);
|
|
// Normally would want to use battle.canDynamax to set this, but it references this property.
|
|
this.canDynamax = (this.battle.gen >= 8);
|
|
this.canGigantamax = this.baseSpecies.canGigantamax || null;
|
|
|
|
// 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() {
|
|
return this.baseMoveSlots.map(moveSlot => moveSlot.id);
|
|
}
|
|
|
|
getSlot() {
|
|
const positionOffset = Math.floor(this.side.n / 2) * this.side.active.length;
|
|
const positionLetter = 'abcdef'.charAt(this.position + positionOffset);
|
|
return this.side.id + positionLetter;
|
|
}
|
|
|
|
toString() {
|
|
const fullname = (this.illusion) ? this.illusion.fullname : this.fullname;
|
|
return this.isActive ? this.getSlot() + fullname.slice(2) : fullname;
|
|
}
|
|
|
|
getDetails = () => {
|
|
const health = this.getHealth();
|
|
let details = this.details;
|
|
if (this.illusion) {
|
|
const illusionDetails = this.illusion.species.name + (this.level === 100 ? '' : ', L' + this.level) +
|
|
(this.illusion.gender === '' ? '' : ', ' + this.illusion.gender) + (this.illusion.set.shiny ? ', shiny' : '');
|
|
details = illusionDetails;
|
|
}
|
|
return {side: health.side, secret: `${details}|${health.secret}`, shared: `${details}|${health.shared}`};
|
|
};
|
|
|
|
updateSpeed() {
|
|
this.speed = this.getActionSpeed();
|
|
}
|
|
|
|
calculateStat(statName: StatNameExceptHP, boost: number, modifier?: number) {
|
|
statName = toID(statName) as StatNameExceptHP;
|
|
// @ts-ignore - 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 BoostName;
|
|
boosts[boostName] = boost;
|
|
boosts = this.battle.runEvent('ModifyBoost', 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: StatNameExceptHP, unboosted?: boolean, unmodified?: boolean) {
|
|
statName = toID(statName) as StatNameExceptHP;
|
|
// @ts-ignore - 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 StatNameExceptHP]?: 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) stat = 10000;
|
|
return stat;
|
|
}
|
|
|
|
getActionSpeed() {
|
|
let speed = this.getStat('spe', false, false);
|
|
if (this.battle.field.getPseudoWeather('trickroom')) {
|
|
speed = 0x2710 - speed;
|
|
}
|
|
return this.battle.trunc(speed, 13);
|
|
}
|
|
|
|
/* 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.getMove(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,
|
|
});
|
|
}
|
|
|
|
allies(): Pokemon[] {
|
|
let allies = this.side.active;
|
|
if (this.battle.gameType === 'multi') {
|
|
const team = this.side.n % 2;
|
|
// @ts-ignore
|
|
allies = this.battle.sides.flatMap(
|
|
(side: Side) => side.n % 2 === team ? side.active : []
|
|
);
|
|
}
|
|
return allies.filter(ally => ally && !ally.fainted);
|
|
}
|
|
|
|
nearbyAllies(): Pokemon[] {
|
|
return this.allies().filter(ally => this.battle.isAdjacent(this, ally));
|
|
}
|
|
|
|
foes(): Pokemon[] {
|
|
let foes = this.side.foe.active;
|
|
if (this.battle.gameType === 'multi') {
|
|
const team = this.side.foe.n % 2;
|
|
// @ts-ignore
|
|
foes = this.battle.sides.flatMap(
|
|
(side: Side) => side.n % 2 === team ? side.active : []
|
|
);
|
|
}
|
|
return foes.filter(foe => foe && !foe.fainted);
|
|
}
|
|
|
|
nearbyFoes(): Pokemon[] {
|
|
return this.foes().filter(foe => this.battle.isAdjacent(this, foe));
|
|
}
|
|
|
|
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.nearbyAllies()[0];
|
|
if (!target2 || target2 === this || !target2.hp) {
|
|
move.smartTarget = false;
|
|
return [target];
|
|
}
|
|
if (!target.hp) {
|
|
move.smartTarget = false;
|
|
return [target2];
|
|
}
|
|
return [target, target2];
|
|
}
|
|
|
|
getMoveTargets(move: ActiveMove, target: Pokemon): {targets: Pokemon[], pressureTargets: Pokemon[]} {
|
|
let targets: Pokemon[] = [];
|
|
let pressureTargets;
|
|
|
|
switch (move.target) {
|
|
case 'all':
|
|
case 'foeSide':
|
|
case 'allySide':
|
|
case 'allyTeam':
|
|
if (!move.target.startsWith('foe')) {
|
|
targets.push(...this.allies());
|
|
}
|
|
if (!move.target.startsWith('ally')) {
|
|
targets.push(...this.foes());
|
|
}
|
|
if (targets.length && !targets.includes(target)) {
|
|
this.battle.retargetLastMove(targets[targets.length - 1]);
|
|
}
|
|
break;
|
|
case 'allAdjacent':
|
|
targets.push(...this.nearbyAllies());
|
|
// falls through
|
|
case 'allAdjacentFoes':
|
|
targets.push(...this.nearbyFoes());
|
|
if (targets.length && !targets.includes(target)) {
|
|
this.battle.retargetLastMove(targets[targets.length - 1]);
|
|
}
|
|
break;
|
|
case 'allies':
|
|
targets = this.allies();
|
|
break;
|
|
default:
|
|
const selectedTarget = target;
|
|
if (!target || (target.fainted && target.side !== this.side)) {
|
|
// If a targeted foe faints, the move is retargeted
|
|
const possibleTarget = this.battle.getRandomTarget(this, move);
|
|
if (!possibleTarget) return {targets: [], pressureTargets: []};
|
|
target = possibleTarget;
|
|
}
|
|
if (target.side.active.length > 1 && !move.tracksTarget) {
|
|
const isCharging = move.flags['charge'] && !this.volatiles['twoturnmove'] &&
|
|
!(move.id.startsWith('solarb') && this.battle.field.isWeather(['sunnyday', 'desolateland'])) &&
|
|
!(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) {
|
|
return {targets: [], pressureTargets: []};
|
|
}
|
|
if (selectedTarget !== target) {
|
|
this.battle.retargetLastMove(target);
|
|
}
|
|
|
|
// Resolve apparent targets for Pressure.
|
|
if (move.pressureTarget) {
|
|
// At the moment, this is the only supported target.
|
|
if (move.pressureTarget === 'foeSide') {
|
|
pressureTargets = this.foes();
|
|
}
|
|
}
|
|
}
|
|
|
|
return {targets, pressureTargets: pressureTargets || targets};
|
|
}
|
|
|
|
ignoringAbility() {
|
|
// Check if any active pokemon have the ability Neutralizing Gas
|
|
let neutralizinggas = 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.abilityData.ending) {
|
|
neutralizinggas = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return !!(
|
|
(this.battle.gen >= 5 && !this.isActive) ||
|
|
((this.volatiles['gastroacid'] || (neutralizinggas && this.ability !== ('neutralizinggas' as ID))) &&
|
|
!this.getAbility().isPermanent
|
|
)
|
|
);
|
|
}
|
|
|
|
ignoringItem() {
|
|
return !!((this.battle.gen >= 5 && !this.isActive) ||
|
|
(this.hasAbility('klutz') && !this.getItem().ignoreKlutz) ||
|
|
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.getMove(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;
|
|
this.lastMoveTargetLoc = targetLoc;
|
|
this.moveThisTurn = move.id;
|
|
}
|
|
|
|
gotAttacked(move: string | Move, damage: number | false | undefined, source: Pokemon) {
|
|
if (!damage) damage = 0;
|
|
move = this.battle.dex.getMove(move);
|
|
this.attackedBy.push({
|
|
source,
|
|
damage,
|
|
move: move.id,
|
|
thisTurn: true,
|
|
});
|
|
}
|
|
|
|
getLastAttackedBy() {
|
|
if (this.attackedBy.length === 0) return undefined;
|
|
return this.attackedBy[this.attackedBy.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(): string | null {
|
|
const lockedMove = this.battle.runEvent('LockMove', this);
|
|
return (lockedMove === true) ? null : lockedMove;
|
|
}
|
|
|
|
getMoves(lockedMove?: string | null, restrictData?: boolean): {
|
|
move: string, id: string, 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',
|
|
}];
|
|
}
|
|
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.getMove(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.getMove(moveSlot.id).basePowerCallback as (pokemon: Pokemon) => number;
|
|
moveName += ' ' + basePowerCallback(this);
|
|
}
|
|
let target = moveSlot.target;
|
|
if (moveSlot.id === 'curse') {
|
|
if (!this.hasType('Ghost')) {
|
|
target = this.battle.dex.getMove('curse').nonGhostTarget || moveSlot.target;
|
|
}
|
|
}
|
|
let disabled = moveSlot.disabled;
|
|
if (this.volatiles['dynamax']) {
|
|
disabled = this.maxMoveDisabled(this.battle.dex.getMove(moveSlot.id));
|
|
} else if (
|
|
(moveSlot.pp <= 0 && !this.volatiles['partialtrappinglock']) || disabled &&
|
|
this.side.active.length >= 2 && this.battle.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 : [];
|
|
}
|
|
|
|
maxMoveDisabled(move: Move) {
|
|
return !!(move.category === 'Status' && (this.hasItem('assaultvest') || this.volatiles['taunt']));
|
|
}
|
|
|
|
getDynamaxRequest(skipChecks?: boolean) {
|
|
// {gigantamax?: string, maxMoves: {[k: string]: string} | null}[]
|
|
if (!skipChecks) {
|
|
if (!this.canDynamax) 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.getMove(moveSlot.id);
|
|
const maxMove = this.battle.getMaxMove(move, this);
|
|
if (maxMove) {
|
|
if (this.maxMoveDisabled(maxMove)) {
|
|
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', target: 'randomNormal', disabled: false}];
|
|
lockedMove = 'struggle';
|
|
}
|
|
|
|
const data: {
|
|
moves: {move: string, id: string, target?: string, disabled?: string | boolean}[],
|
|
maybeDisabled?: boolean,
|
|
trapped?: boolean,
|
|
maybeTrapped?: boolean,
|
|
canMegaEvo?: boolean,
|
|
canUltraBurst?: boolean,
|
|
canZMove?: AnyObject | null,
|
|
canDynamax?: boolean,
|
|
maxMoves?: DynamaxOptions,
|
|
} = {
|
|
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.canUltraBurst) data.canUltraBurst = true;
|
|
const canZMove = this.battle.canZMove(this);
|
|
if (canZMove) data.canZMove = canZMove;
|
|
|
|
if (this.getDynamaxRequest()) data.canDynamax = true;
|
|
if (data.canDynamax || this.volatiles['dynamax']) data.maxMoves = this.getDynamaxRequest(true);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
getSwitchRequestData() {
|
|
const entry: AnyObject = {
|
|
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.moves.map(move => {
|
|
if (move === 'hiddenpower') {
|
|
return move + toID(this.hpType) + (this.battle.gen < 6 ? '' : this.hpPower);
|
|
}
|
|
if (move === 'frustration' || move === 'return') {
|
|
const basePowerCallback = this.battle.dex.getMove(move).basePowerCallback as (pokemon: Pokemon) => number;
|
|
return move + basePowerCallback(this);
|
|
}
|
|
return move;
|
|
}),
|
|
baseAbility: this.baseAbility,
|
|
item: this.item,
|
|
pokeball: this.pokeball,
|
|
};
|
|
if (this.battle.gen > 6) entry.ability = this.ability;
|
|
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: BoostName;
|
|
for (boost in this.boosts) {
|
|
if (this.boosts[boost] > 0) boosts += this.boosts[boost];
|
|
}
|
|
return boosts;
|
|
}
|
|
|
|
boostBy(boosts: SparseBoostsTable) {
|
|
let delta = 0;
|
|
let boostName: BoostName;
|
|
for (boostName in boosts) {
|
|
delta = boosts[boostName]!;
|
|
this.boosts[boostName] += delta;
|
|
if (this.boosts[boostName] > 6) {
|
|
delta -= this.boosts[boostName] - 6;
|
|
this.boosts[boostName] = 6;
|
|
}
|
|
if (this.boosts[boostName] < -6) {
|
|
delta -= this.boosts[boostName] - (-6);
|
|
this.boosts[boostName] = -6;
|
|
}
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
clearBoosts() {
|
|
let boostName: BoostName;
|
|
for (boostName in this.boosts) {
|
|
this.boosts[boostName] = 0;
|
|
}
|
|
}
|
|
|
|
setBoost(boosts: SparseBoostsTable) {
|
|
let boostName: BoostName;
|
|
for (boostName in boosts) {
|
|
this.boosts[boostName] = boosts[boostName]!;
|
|
}
|
|
}
|
|
|
|
copyVolatileFrom(pokemon: Pokemon) {
|
|
this.clearVolatile();
|
|
this.boosts = pokemon.boosts;
|
|
for (const i in pokemon.volatiles) {
|
|
if (this.battle.dex.getEffectByID(i as ID).noCopy) continue;
|
|
// shallow clones
|
|
this.volatiles[i] = {...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 || 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') {
|
|
return false;
|
|
}
|
|
|
|
if (!this.setSpecies(species, effect, true)) return false;
|
|
|
|
this.transformed = true;
|
|
this.weighthg = pokemon.weighthg;
|
|
|
|
const types = pokemon.getTypes(true);
|
|
this.setType(pokemon.volatiles['roost'] ? pokemon.volatiles['roost'].typeWas : types, true);
|
|
this.addedType = pokemon.addedType;
|
|
this.knownType = this.side === pokemon.side && pokemon.knownType;
|
|
this.apparentType = pokemon.apparentType;
|
|
|
|
let statName: StatNameExceptHP;
|
|
for (statName in this.storedStats) {
|
|
this.storedStats[statName] = pokemon.storedStats[statName];
|
|
}
|
|
this.moveSlots = [];
|
|
this.set.ivs = (this.battle.gen >= 5 ? this.set.ivs : pokemon.set.ivs);
|
|
this.hpType = (this.battle.gen >= 5 ? this.hpType : pokemon.hpType);
|
|
this.hpPower = (this.battle.gen >= 5 ? this.hpPower : pokemon.hpPower);
|
|
for (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: BoostName;
|
|
for (boostName in pokemon.boosts) {
|
|
this.boosts[boostName] = pokemon.boosts[boostName]!;
|
|
}
|
|
if (this.battle.gen >= 6) {
|
|
const volatilesToCopy = ['focusenergy', 'gmaxchistrike', 'laserfocus'];
|
|
for (const volatile of volatilesToCopy) {
|
|
if (pokemon.volatiles[volatile]) {
|
|
this.addVolatile(volatile);
|
|
if (volatile === 'gmaxchistrike') this.volatiles[volatile].layers = pokemon.volatiles[volatile].layers;
|
|
} else {
|
|
this.removeVolatile(volatile);
|
|
}
|
|
}
|
|
}
|
|
if (effect) {
|
|
this.battle.add('-transform', this, pokemon, '[from] ' + effect.fullname);
|
|
} else {
|
|
this.battle.add('-transform', this, pokemon);
|
|
}
|
|
if (this.battle.gen > 2) this.setAbility(pokemon.ability, this, 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Changes this Pokemon's species to the given speciesId (or species).
|
|
* This function only handles changes to stats and type.
|
|
* Use formChange 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: StatNameExceptHP;
|
|
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 = this.battle.effect,
|
|
isPermanent?: boolean, message?: string
|
|
) {
|
|
const rawSpecies = this.battle.dex.getSpecies(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) {
|
|
this.baseSpecies = rawSpecies;
|
|
this.details = species.name + (this.level === 100 ? '' : ', L' + this.level) +
|
|
(this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : '');
|
|
this.battle.add('detailschange', this, (this.illusion || this).details);
|
|
if (source.effectType === 'Item') {
|
|
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.onPrimal) {
|
|
if (this.illusion) {
|
|
this.ability = '';
|
|
this.battle.add('-primal', this.illusion);
|
|
} else {
|
|
this.battle.add('-primal', this);
|
|
}
|
|
} 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 && !['disguise', 'iceface'].includes(source.id)) {
|
|
if (this.illusion) {
|
|
this.ability = ''; // Don't allow Illusion to wear off
|
|
}
|
|
this.setAbility(species.abilities['0'], null, true);
|
|
this.baseAbility = this.ability;
|
|
}
|
|
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;
|
|
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;
|
|
this.moveThisTurn = '';
|
|
|
|
this.lastDamage = 0;
|
|
this.attackedBy = [];
|
|
this.hurtThisTurn = false;
|
|
this.newlySwitched = true;
|
|
this.beingCalledBack = false;
|
|
|
|
this.volatileStaleness = undefined;
|
|
|
|
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 ? sourceEffect.fullname : '');
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 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 = 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.hasAbility('comatose') && 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.getEffect(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 prevStatusData = this.statusData;
|
|
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.statusData = {id: status.id, target: this};
|
|
if (source) this.statusData.source = source;
|
|
if (status.duration) this.statusData.duration = status.duration;
|
|
if (status.durationCallback) {
|
|
this.statusData.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
|
|
if (status.id && !this.battle.singleEvent('Start', status, this.statusData, this, source, sourceEffect)) {
|
|
this.battle.debug('status start [' + status.id + '] interrupted');
|
|
// cancel the setstatus
|
|
this.status = prevStatus;
|
|
this.statusData = prevStatusData;
|
|
return false;
|
|
}
|
|
if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Unlike cureStatus, does not give cure message
|
|
*/
|
|
clearStatus() {
|
|
return this.setStatus('');
|
|
}
|
|
|
|
getStatus() {
|
|
return this.battle.dex.getEffectByID(this.status);
|
|
}
|
|
|
|
eatItem(force?: boolean, source?: Pokemon, sourceEffect?: Effect) {
|
|
if (!this.hp || !this.isActive) return false;
|
|
if (!this.item) return false;
|
|
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
|
|
const item = this.getItem();
|
|
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.itemData, 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.itemData = {id: '', target: this};
|
|
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) return false;
|
|
|
|
if (!sourceEffect && this.battle.effect) sourceEffect = this.battle.effect;
|
|
if (!source && this.battle.event && this.battle.event.target) source = this.battle.event.target;
|
|
const item = this.getItem();
|
|
if (this.battle.runEvent('UseItem', this, null, null, item)) {
|
|
switch (item.id) {
|
|
case 'redcard':
|
|
this.battle.add('-enditem', this, item, '[of] ' + source);
|
|
break;
|
|
default:
|
|
if (item.isGem) {
|
|
this.battle.add('-enditem', this, item, '[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.itemData, this, source, sourceEffect);
|
|
|
|
this.lastItem = this.item;
|
|
this.item = '';
|
|
this.itemData = {id: '', target: this};
|
|
this.usedItemThisTurn = true;
|
|
this.battle.runEvent('AfterUseItem', this, null, null, item);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
takeItem(source?: Pokemon) {
|
|
if (!this.isActive) return false;
|
|
if (!this.item) return false;
|
|
if (!source) source = this;
|
|
if (this.battle.gen === 4) {
|
|
if (toID(this.ability) === 'multitype') return false;
|
|
if (source && toID(source.ability) === 'multitype') return false;
|
|
}
|
|
const item = this.getItem();
|
|
if (this.battle.runEvent('TakeItem', this, source, null, item)) {
|
|
this.item = '';
|
|
this.itemData = {id: '', target: this};
|
|
this.pendingStaleness = undefined;
|
|
return item;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
setItem(item: string | Item, source?: Pokemon, effect?: Effect) {
|
|
if (!this.hp || !this.isActive) return false;
|
|
if (typeof item === 'string') item = this.battle.dex.getItem(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.side.id !== this.side.id;
|
|
this.pendingStaleness = external ? 'external' : 'internal';
|
|
} else {
|
|
this.pendingStaleness = undefined;
|
|
}
|
|
this.item = item.id;
|
|
this.itemData = {id: item.id, target: this};
|
|
if (item.id) {
|
|
this.battle.singleEvent('Start', item, this.itemData, this, source, effect);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
getItem() {
|
|
return this.battle.dex.getItem(this.item);
|
|
}
|
|
|
|
hasItem(item: string | string[]) {
|
|
if (this.ignoringItem()) return false;
|
|
const ownItem = this.item;
|
|
if (!Array.isArray(item)) return ownItem === toID(item);
|
|
return item.map(toID).includes(ownItem);
|
|
}
|
|
|
|
clearItem() {
|
|
return this.setItem('');
|
|
}
|
|
|
|
setAbility(ability: string | Ability, source?: Pokemon | null, isFromFormeChange?: boolean) {
|
|
if (!this.hp) return false;
|
|
if (typeof ability === 'string') ability = this.battle.dex.getAbility(ability);
|
|
const oldAbility = this.ability;
|
|
if (!isFromFormeChange) {
|
|
if (ability.isPermanent || this.getAbility().isPermanent) return false;
|
|
}
|
|
if (!this.battle.runEvent('SetAbility', this, source, this.battle.effect, ability)) return false;
|
|
this.battle.singleEvent('End', this.battle.dex.getAbility(oldAbility), this.abilityData, this, source);
|
|
if (this.battle.effect && this.battle.effect.effectType === 'Move') {
|
|
this.battle.add('-endability', this, this.battle.dex.getAbility(oldAbility), '[from] move: ' +
|
|
this.battle.dex.getMove(this.battle.effect.id));
|
|
}
|
|
this.ability = ability.id;
|
|
this.abilityData = {id: ability.id, target: this};
|
|
if (ability.id && this.battle.gen > 3) {
|
|
this.battle.singleEvent('Start', ability, this.abilityData, this, source);
|
|
}
|
|
this.abilityOrder = this.battle.abilityOrder++;
|
|
return oldAbility;
|
|
}
|
|
|
|
getAbility() {
|
|
return this.battle.dex.getAbility(this.ability);
|
|
}
|
|
|
|
hasAbility(ability: string | string[]) {
|
|
if (this.ignoringAbility()) return false;
|
|
const ownAbility = this.ability;
|
|
if (!Array.isArray(ability)) return ownAbility === toID(ability);
|
|
return ability.map(toID).includes(ownAbility);
|
|
}
|
|
|
|
clearAbility() {
|
|
return this.setAbility('');
|
|
}
|
|
|
|
getNature() {
|
|
return this.battle.dex.getNature(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.getEffect(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] = {id: status.id};
|
|
this.volatiles[status.id].target = this;
|
|
if (source) {
|
|
this.volatiles[status.id].source = source;
|
|
this.volatiles[status.id].sourcePosition = source.position;
|
|
}
|
|
if (sourceEffect) this.volatiles[status.id].sourceEffect = sourceEffect;
|
|
if (status.duration) this.volatiles[status.id].duration = status.duration;
|
|
if (status.durationCallback) {
|
|
this.volatiles[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
result = this.battle.singleEvent('Start', status, this.volatiles[status.id], this, source, sourceEffect);
|
|
if (!result) {
|
|
// cancel
|
|
delete this.volatiles[status.id];
|
|
return result;
|
|
}
|
|
if (linkedStatus && source) {
|
|
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.getEffect(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.getEffect(status) as Effect;
|
|
if (!this.volatiles[status.id]) return false;
|
|
this.battle.singleEvent('End', status, this.volatiles[status.id], this);
|
|
const linkedPokemon = this.volatiles[status.id].linkedPokemon;
|
|
const linkedStatus = this.volatiles[status.id].linkedStatus;
|
|
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) {
|
|
// 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)
|
|
* newType can be an array, but this is for OMs only. The game in
|
|
* reality doesn't support setting a type to more than one type.
|
|
*/
|
|
setType(newType: string | string[], enforce = false) {
|
|
// First type of Arceus, Silvally cannot be normally changed
|
|
if (!enforce) {
|
|
if ((this.battle.gen >= 5 && (this.species.num === 493 || this.species.num === 773)) ||
|
|
(this.battle.gen === 4 && this.hasAbility('multitype'))) {
|
|
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) {
|
|
this.addedType = newType;
|
|
return true;
|
|
}
|
|
|
|
getTypes(excludeAdded?: boolean): string[] {
|
|
const types = this.battle.runEvent('Type', this, null, null, this.types);
|
|
if (!excludeAdded && this.addedType) return types.concat(this.addedType);
|
|
if (types.length) return types;
|
|
return [this.battle.gen >= 5 ? 'Normal' : '???'];
|
|
}
|
|
|
|
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') && !('roost' in this.volatiles)) return false;
|
|
if (this.hasAbility('levitate') && !this.battle.suppressingAttackEvents()) return null;
|
|
if ('magnetrise' in this.volatiles) return false;
|
|
if ('telekinesis' in this.volatiles) return false;
|
|
return item !== 'airballoon';
|
|
}
|
|
|
|
isSemiInvulnerable() {
|
|
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']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
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);
|
|
}
|
|
return totalTypeMod;
|
|
}
|
|
|
|
/** false = immune, true = not immune */
|
|
runImmunity(type: string, message?: string | boolean) {
|
|
if (!type || type === '???') return true;
|
|
if (!(type in this.battle.dex.data.TypeChart)) {
|
|
if (type === 'Fairy' || type === 'Dark' || type === 'Steel') return true;
|
|
throw new Error("Use runStatusImmunity for " + type);
|
|
}
|
|
if (this.fainted) return false;
|
|
|
|
const negateResult = this.battle.runEvent('NegateImmunity', this, type);
|
|
let isGrounded;
|
|
if (type === 'Ground') {
|
|
isGrounded = this.isGrounded(!negateResult);
|
|
if (isGrounded === null) {
|
|
if (message) {
|
|
this.battle.add('-immune', this, '[from] ability: Levitate');
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
if (!negateResult) return true;
|
|
if ((isGrounded === undefined && !this.battle.dex.getImmunity(type, this)) || isGrounded === false) {
|
|
if (message) {
|
|
this.battle.add('-immune', this);
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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!;
|
|
}
|
|
}
|